diff --git a/WindowsAgent/Tools/NuGet.exe b/WindowsAgent/Tools/NuGet.exe new file mode 100644 index 0000000..4645f4b Binary files /dev/null and b/WindowsAgent/Tools/NuGet.exe differ diff --git a/WindowsAgent/WindowsAgent.sln b/WindowsAgent/WindowsAgent.sln new file mode 100644 index 0000000..71a494b --- /dev/null +++ b/WindowsAgent/WindowsAgent.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsAgent", "WindowsAgent\WindowsAgent.csproj", "{F7E2A8D5-6D24-4651-A4BC-1024D59F4903}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F7E2A8D5-6D24-4651-A4BC-1024D59F4903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7E2A8D5-6D24-4651-A4BC-1024D59F4903}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7E2A8D5-6D24-4651-A4BC-1024D59F4903}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7E2A8D5-6D24-4651-A4BC-1024D59F4903}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/WindowsAgent/WindowsAgent/App.config b/WindowsAgent/WindowsAgent/App.config new file mode 100644 index 0000000..3df6972 --- /dev/null +++ b/WindowsAgent/WindowsAgent/App.config @@ -0,0 +1,30 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WindowsAgent/WindowsAgent/ExecutionPlan.cs b/WindowsAgent/WindowsAgent/ExecutionPlan.cs new file mode 100644 index 0000000..899c5cc --- /dev/null +++ b/WindowsAgent/WindowsAgent/ExecutionPlan.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Mirantis.Keero.WindowsAgent +{ + class ExecutionPlan + { + public class Command + { + public string Name { get; set; } + public Dictionary Arguments { get; set; } + } + + public string[] Scripts { get; set; } + public LinkedList Commands { get; set; } + public int RebootOnCompletion { get; set; } + } +} diff --git a/WindowsAgent/WindowsAgent/MqMessage.cs b/WindowsAgent/WindowsAgent/MqMessage.cs new file mode 100644 index 0000000..d77ab79 --- /dev/null +++ b/WindowsAgent/WindowsAgent/MqMessage.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Mirantis.Keero.WindowsAgent +{ + class MqMessage + { + private readonly Action ackFunc; + + public MqMessage(Action ackFunc) + { + this.ackFunc = ackFunc; + } + + public string Body { get; set; } + + public void Ack() + { + ackFunc(); + } + } +} diff --git a/WindowsAgent/WindowsAgent/PlanExecutor.cs b/WindowsAgent/WindowsAgent/PlanExecutor.cs new file mode 100644 index 0000000..ad4910c --- /dev/null +++ b/WindowsAgent/WindowsAgent/PlanExecutor.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Text; +using Newtonsoft.Json; + +namespace Mirantis.Keero.WindowsAgent +{ + class PlanExecutor + { + class ExecutionResult + { + public bool IsException { get; set; } + public object Result { get; set; } + } + + private readonly string path; + + public PlanExecutor(string path) + { + this.path = path; + } + + public bool RebootNeeded { get; set; } + + public string Execute() + { + RebootNeeded = false; + try + { + var plan = JsonConvert.DeserializeObject(File.ReadAllText(this.path)); + var resultPath = this.path + ".result"; + List currentResults = null; + try + { + currentResults = JsonConvert.DeserializeObject>(File.ReadAllText(resultPath)); + } + catch + { + currentResults = new List(); + } + + + var runSpace = RunspaceFactory.CreateRunspace(); + runSpace.Open(); + + var runSpaceInvoker = new RunspaceInvoke(runSpace); + runSpaceInvoker.Invoke("Set-ExecutionPolicy Unrestricted"); + if (plan.Scripts != null) + { + foreach (var script in plan.Scripts) + { + runSpaceInvoker.Invoke(Encoding.UTF8.GetString(Convert.FromBase64String(script))); + } + } + + while (plan.Commands != null && plan.Commands.Any()) + { + var command = plan.Commands.First(); + + var pipeline = runSpace.CreatePipeline(); + var psCommand = new Command(command.Name); + if (command.Arguments != null) + { + foreach (var kvp in command.Arguments) + { + psCommand.Parameters.Add(kvp.Key, kvp.Value); + } + } + pipeline.Commands.Add(psCommand); + try + { + var result = pipeline.Invoke(); + if (result != null) + { + currentResults.Add(new ExecutionResult { + IsException = false, + Result = result.Select(SerializePsObject).Where(obj => obj != null).ToList() + }); + } + } + catch (Exception exception) + { + currentResults.Add(new ExecutionResult { + IsException = true, + Result = new[] { + exception.GetType().FullName, exception.Message + } + }); + } + finally + { + plan.Commands.RemoveFirst(); + File.WriteAllText(path, JsonConvert.SerializeObject(plan)); + File.WriteAllText(resultPath, JsonConvert.SerializeObject(currentResults)); + } + } + runSpace.Close(); + var executionResult = JsonConvert.SerializeObject(new ExecutionResult { + IsException = false, + Result = currentResults + }, Formatting.Indented); + + if (plan.RebootOnCompletion > 0) + { + if (plan.RebootOnCompletion == 1) + { + RebootNeeded = !currentResults.Any(t => t.IsException); + } + else + { + RebootNeeded = true; + } + } + + File.Delete(resultPath); + return executionResult; + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new ExecutionResult { + IsException = true, + Result = ex.Message + }, Formatting.Indented); + } + } + + private static object SerializePsObject(PSObject obj) + { + if (obj.BaseObject is PSCustomObject) + { + var result = new Dictionary(); + foreach (var property in obj.Properties.Where(p => p.IsGettable)) + { + try + { + result[property.Name] = property.Value.ToString(); + } + catch + { + } + } + return result; + } + else + { + return obj.BaseObject; + } + } + } + +} diff --git a/WindowsAgent/WindowsAgent/Program.cs b/WindowsAgent/WindowsAgent/Program.cs new file mode 100644 index 0000000..d228f92 --- /dev/null +++ b/WindowsAgent/WindowsAgent/Program.cs @@ -0,0 +1,97 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Threading; +using NLog; + +namespace Mirantis.Keero.WindowsAgent +{ + [DisplayName("Keero Agent")] + sealed public class Program : WindowsService + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private volatile bool stop; + private Thread thread; + private RabbitMqClient rabbitMqClient; + + static void Main(string[] args) + { + Start(new Program(), args); + } + + protected override void OnStart(string[] args) + { + base.OnStart(args); + this.rabbitMqClient = new RabbitMqClient(); + this.thread = new Thread(Loop); + this.thread.Start(); + } + + void Loop() + { + var doReboot = false; + const string filePath = "data.json"; + while (!stop) + { + try + { + if (!File.Exists(filePath)) + { + var message = rabbitMqClient.GetMessage(); + File.WriteAllText(filePath, message.Body); + message.Ack(); + } + var executor = new PlanExecutor(filePath); + var result = executor.Execute(); + if(stop) break; + rabbitMqClient.SendResult(result); + File.Delete(filePath); + if (executor.RebootNeeded) + { + doReboot = true; + break; + } + } + catch (Exception exception) + { + WaitOnException(exception); + } + + } + if (doReboot) + { + Console.WriteLine("Rebooting..."); + try + { + System.Diagnostics.Process.Start("shutdown.exe", "-r -t 0"); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + + } + + private void WaitOnException(Exception exception) + { + if (stop) return; + Log.WarnException("Exception in main loop", exception); + var i = 0; + while (!stop && i < 10) + { + Thread.Sleep(100); + i++; + } + } + + protected override void OnStop() + { + stop = true; + this.rabbitMqClient.Dispose(); + Console.WriteLine("Stop"); + base.OnStop(); + } + + } +} diff --git a/WindowsAgent/WindowsAgent/Properties/AssemblyInfo.cs b/WindowsAgent/WindowsAgent/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f7d169b --- /dev/null +++ b/WindowsAgent/WindowsAgent/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WindowsAgent")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WindowsAgent")] +[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9591bf2c-f38b-47e0-a39d-ea9849356371")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/WindowsAgent/WindowsAgent/RabbitMqClient.cs b/WindowsAgent/WindowsAgent/RabbitMqClient.cs new file mode 100644 index 0000000..0ce1a77 --- /dev/null +++ b/WindowsAgent/WindowsAgent/RabbitMqClient.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RabbitMQ.Client; + +namespace Mirantis.Keero.WindowsAgent +{ + class RabbitMqClient : IDisposable + { + private static readonly ConnectionFactory connectionFactory; + private IConnection currentConnecton; + + static RabbitMqClient() + { + connectionFactory = new ConnectionFactory { + HostName = ConfigurationManager.AppSettings["rabbitmq.host"] ?? "localhost", + UserName = ConfigurationManager.AppSettings["rabbitmq.user"] ?? "guest", + Password = ConfigurationManager.AppSettings["rabbitmq.password"] ??"guest", + Protocol = Protocols.FromEnvironment(), + VirtualHost = ConfigurationManager.AppSettings["rabbitmq.vhost"] ?? "/", + RequestedHeartbeat = 10 + }; + } + + public RabbitMqClient() + { + + } + + public MqMessage GetMessage() + { + var queueName = ConfigurationManager.AppSettings["rabbitmq.inputQueue"] ?? Environment.MachineName.ToLower(); + try + { + IConnection connection = null; + lock (this) + { + connection = this.currentConnecton = this.currentConnecton ?? connectionFactory.CreateConnection(); + } + var session = connection.CreateModel(); + session.BasicQos(0, 1, false); + session.QueueDeclare(queueName, true, false, false, null); + var consumer = new QueueingBasicConsumer(session); + var consumeTag = session.BasicConsume(queueName, false, consumer); + var e = (RabbitMQ.Client.Events.BasicDeliverEventArgs)consumer.Queue.Dequeue(); + Action ackFunc = delegate { + session.BasicAck(e.DeliveryTag, false); + session.BasicCancel(consumeTag); + session.Close(); + }; + + return new MqMessage(ackFunc) { + Body = Encoding.UTF8.GetString(e.Body) + }; + } + catch (Exception) + { + + Dispose(); + throw; + } + } + + public void SendResult(string text) + { + var exchangeName = ConfigurationManager.AppSettings["rabbitmq.resultExchange"] ?? ""; + var resultQueue = ConfigurationManager.AppSettings["rabbitmq.resultQueue"] ?? "-execution-results"; + + try + { + IConnection connection = null; + lock (this) + { + connection = this.currentConnecton = this.currentConnecton ?? connectionFactory.CreateConnection(); + } + var session = connection.CreateModel(); + if (!string.IsNullOrEmpty(resultQueue)) + { + session.QueueDeclare(resultQueue, true, false, false, null); + if (!string.IsNullOrEmpty(exchangeName)) + { + session.ExchangeBind(exchangeName, resultQueue, resultQueue); + } + } + var basicProperties = session.CreateBasicProperties(); + basicProperties.SetPersistent(true); + basicProperties.ContentType = "application/json"; + session.BasicPublish(exchangeName, resultQueue, basicProperties, Encoding.UTF8.GetBytes(text)); + session.Close(); + } + catch (Exception) + { + Dispose(); + throw; + } + } + + public void Dispose() + { + lock (this) + { + try + { + if (this.currentConnecton != null) + { + this.currentConnecton.Close(); + } + } + catch + { + } + finally + { + this.currentConnecton = null; + } + } + } + } +} diff --git a/WindowsAgent/WindowsAgent/SampleExecutionPlan.json b/WindowsAgent/WindowsAgent/SampleExecutionPlan.json new file mode 100644 index 0000000..9522b70 --- /dev/null +++ b/WindowsAgent/WindowsAgent/SampleExecutionPlan.json @@ -0,0 +1,37 @@ +{ + "Scripts": + [ + "ZnVuY3Rpb24gdDMgeyAxMjsgcmV0dXJuICJ0ZXN0IiB9", + "ZnVuY3Rpb24gTmV3LVBlcnNvbigpDQp7DQogIHBhcmFtICgkRmlyc3ROYW1lLCAkTGFzdE5hbWUsICRQaG9uZSkNCg0KICAkcGVyc29uID0gbmV3LW9iamVjdCBQU09iamVjdA0KDQogICRwZXJzb24gfCBhZGQtbWVtYmVyIC10eXBlIE5vdGVQcm9wZXJ0eSAtTmFtZSBGaXJzdCAtVmFsdWUgJEZpcnN0TmFtZQ0KICAkcGVyc29uIHwgYWRkLW1lbWJlciAtdHlwZSBOb3RlUHJvcGVydHkgLU5hbWUgTGFzdCAtVmFsdWUgJExhc3ROYW1lDQogICRwZXJzb24gfCBhZGQtbWVtYmVyIC10eXBlIE5vdGVQcm9wZXJ0eSAtTmFtZSBQaG9uZSAtVmFsdWUgJFBob25lDQoNCiAgcmV0dXJuICRwZXJzb24NCn0=", + "ZnVuY3Rpb24gVGVzdFRocm93KCkNCnsNCglUaHJvdyBbc3lzdGVtLkluZGV4T3V0T2ZSYW5nZUV4Y2VwdGlvbl0gDQp9" + ], + "Commands" : + [ + { + "Name": "New-Person", + "Arguments" : + { + "FirstName": "MyFirstName", + "LastName": "MyLastName", + "Phone": "123-456" + } + + }, + { + "Name": "t3", + "Arguments" : + { + } + + }, + { + "Name": "Get-Date", + + }, + { + "Name": "TestThrow", + + } + ], + "RebootOnCompletion": 0 +} \ No newline at end of file diff --git a/WindowsAgent/WindowsAgent/ServiceManager.cs b/WindowsAgent/WindowsAgent/ServiceManager.cs new file mode 100644 index 0000000..2bcf227 --- /dev/null +++ b/WindowsAgent/WindowsAgent/ServiceManager.cs @@ -0,0 +1,111 @@ +using System; +using System.Configuration.Install; +using System.Reflection; +using System.ServiceProcess; +using NLog; + +namespace Mirantis.Keero.WindowsAgent +{ + public class ServiceManager + { + private readonly string serviceName; + + public ServiceManager(string serviceName) + { + this.serviceName = serviceName; + } + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + public bool Restart(string[] args, TimeSpan timeout) + { + var service = new ServiceController(serviceName); + try + { + var millisec1 = TimeSpan.FromMilliseconds(Environment.TickCount); + + service.Stop(); + service.WaitForStatus(ServiceControllerStatus.Stopped, timeout); + Log.Info("Service is stopped"); + + // count the rest of the timeout + var millisec2 = TimeSpan.FromMilliseconds(Environment.TickCount); + timeout = timeout - (millisec2 - millisec1); + + service.Start(args); + service.WaitForStatus(ServiceControllerStatus.Running, timeout); + Log.Info("Service has started"); + return true; + } + catch (Exception ex) + { + Log.ErrorException("Cannot restart service " + serviceName, ex); + return false; + } + } + + public bool Stop(TimeSpan timeout) + { + var service = new ServiceController(serviceName); + try + { + service.Stop(); + service.WaitForStatus(ServiceControllerStatus.Stopped, timeout); + return true; + } + catch (Exception ex) + { + Log.ErrorException("Cannot stop service " + serviceName, ex); + return false; + } + } + + public bool Start(string[] args, TimeSpan timeout) + { + var service = new ServiceController(serviceName); + try + { + service.Start(args); + service.WaitForStatus(ServiceControllerStatus.Running, timeout); + return true; + } + catch (Exception ex) + { + Log.ErrorException("Cannot start service " + serviceName, ex); + return false; + } + } + + public bool Install() + { + try + { + ManagedInstallerClass.InstallHelper( + new string[] { Assembly.GetEntryAssembly().Location }); + } + catch(Exception ex) + { + Log.ErrorException("Cannot install service " + serviceName, ex); + return false; + } + return true; + } + + public bool Uninstall() + { + try + { + ManagedInstallerClass.InstallHelper( + new string[] { "/u", Assembly.GetEntryAssembly().Location }); + } + catch (Exception ex) + { + Log.ErrorException("Cannot uninstall service " + serviceName, ex); + return false; + } + return true; + } + + } + +} diff --git a/WindowsAgent/WindowsAgent/WindowsAgent.csproj b/WindowsAgent/WindowsAgent/WindowsAgent.csproj new file mode 100644 index 0000000..60247a2 --- /dev/null +++ b/WindowsAgent/WindowsAgent/WindowsAgent.csproj @@ -0,0 +1,92 @@ + + + + + Debug + AnyCPU + {F7E2A8D5-6D24-4651-A4BC-1024D59F4903} + Exe + Properties + Mirantis.Keero.WindowsAgent + WindowsAgent + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + + + ..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll + + + ..\packages\RabbitMQ.Client.3.0.2\lib\net30\RabbitMQ.Client.dll + + + + + + + False + C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll + + + + + + + + + + + + + + Component + + + + + + Component + + + Component + + + + + + + + + + $(SolutionDir)Tools\nuget install $(ProjectDir)packages.config -o $(SolutionDir)Packages + + + \ No newline at end of file diff --git a/WindowsAgent/WindowsAgent/WindowsService.cs b/WindowsAgent/WindowsAgent/WindowsService.cs new file mode 100644 index 0000000..1ad2d0d --- /dev/null +++ b/WindowsAgent/WindowsAgent/WindowsService.cs @@ -0,0 +1,95 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.ServiceProcess; +using NLog; + +namespace Mirantis.Keero.WindowsAgent +{ + public abstract class WindowsService : ServiceBase + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + public bool RunningAsService { get; private set; } + + protected static void Start(WindowsService service, string[] arguments) + { + Directory.SetCurrentDirectory(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)); + + if (arguments.Contains("/install", StringComparer.OrdinalIgnoreCase)) + { + new ServiceManager(service.ServiceName).Install(); + } + else if (arguments.Contains("/uninstall", StringComparer.OrdinalIgnoreCase)) + { + new ServiceManager(service.ServiceName).Uninstall(); + } + else if (arguments.Contains("/start", StringComparer.OrdinalIgnoreCase)) + { + new ServiceManager(service.ServiceName).Start(Environment.GetCommandLineArgs(), TimeSpan.FromMinutes(1)); + } + else if (arguments.Contains("/stop", StringComparer.OrdinalIgnoreCase)) + { + new ServiceManager(service.ServiceName).Stop(TimeSpan.FromMinutes(1)); + } + else if (arguments.Contains("/restart", StringComparer.OrdinalIgnoreCase)) + { + new ServiceManager(service.ServiceName).Restart(Environment.GetCommandLineArgs(), TimeSpan.FromMinutes(1)); + } + else if (!arguments.Contains("/console", StringComparer.OrdinalIgnoreCase)) + { + service.RunningAsService = true; + Run(service); + } + else + { + try + { + service.RunningAsService = false; + Console.Title = service.ServiceName; + service.OnStart(Environment.GetCommandLineArgs()); + service.WaitForExitSignal(); + } + finally + { + service.OnStop(); + service.Dispose(); + } + } + } + + protected WindowsService() + { + var displayNameAttribute = + this.GetType().GetCustomAttributes(typeof (DisplayNameAttribute), false).Cast(). + FirstOrDefault(); + if(displayNameAttribute != null) + { + ServiceName = displayNameAttribute.DisplayName; + } + } + + + protected virtual void WaitForExitSignal() + { + Console.WriteLine("Press ESC to exit"); + while (Console.ReadKey(true).Key != ConsoleKey.Escape) + { + } + } + + protected override void OnStart(string[] args) + { + Log.Info("Service {0} started", ServiceName); + + base.OnStart(args); + } + + protected override void OnStop() + { + Log.Info("Service {0} exited", ServiceName); + base.OnStop(); + } + } +} diff --git a/WindowsAgent/WindowsAgent/WindowsServiceInstaller.cs b/WindowsAgent/WindowsAgent/WindowsServiceInstaller.cs new file mode 100644 index 0000000..ca6e4c2 --- /dev/null +++ b/WindowsAgent/WindowsAgent/WindowsServiceInstaller.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; +using System.Configuration.Install; +using System.Linq; +using System.Reflection; +using System.ServiceProcess; + +namespace Mirantis.Keero.WindowsAgent +{ + [RunInstaller(true)] + public class WindowsServiceInstaller : Installer + { + public WindowsServiceInstaller() + { + var processInstaller = new ServiceProcessInstaller { Account = ServiceAccount.LocalSystem }; + foreach (var type in Assembly.GetEntryAssembly().GetExportedTypes().Where(t => t.IsSubclassOf(typeof(ServiceBase)))) + { + var nameAttribute = type.GetCustomAttributes(typeof (DisplayNameAttribute), false) + .Cast().FirstOrDefault(); + if(nameAttribute == null) continue; + var serviceInstaller = new ServiceInstaller { + StartType = ServiceStartMode.Automatic, + ServiceName = nameAttribute.DisplayName, + DisplayName = nameAttribute.DisplayName + }; + var descriptionAttribute = type.GetCustomAttributes(typeof(DescriptionAttribute), false) + .Cast().FirstOrDefault(); + if(descriptionAttribute != null) + { + serviceInstaller.Description = descriptionAttribute.Description; + } + + Installers.Add(serviceInstaller); + } + + Installers.Add(processInstaller); + + } + } +} diff --git a/WindowsAgent/WindowsAgent/packages.config b/WindowsAgent/WindowsAgent/packages.config new file mode 100644 index 0000000..7aabef8 --- /dev/null +++ b/WindowsAgent/WindowsAgent/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/WindowsAgent/packages/repositories.config b/WindowsAgent/packages/repositories.config new file mode 100644 index 0000000..7753eee --- /dev/null +++ b/WindowsAgent/packages/repositories.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/dashboard/ReadMe.txt b/dashboard/ReadMe.txt new file mode 100644 index 0000000..45d191b --- /dev/null +++ b/dashboard/ReadMe.txt @@ -0,0 +1,31 @@ +# TO DO: +# 1. Add new functional for services and data centers +# 2. Fix issue with list of services: services table shoudl show services for +# specific data center + +This file is described how to install new tab on horizon dashboard. +We should do the following: + 1. Copy directory 'windc' to directory '/opt/stack/horizon/openstack_dashboard/dashboards/project' + 2. Copy api/windc.py to directory '/opt/stack/horizon/openstack_dashboard/api' + 3. Copy directory 'windcclient' to directory '/opt/stack/horizon/' + 4. Edit file '/opt/stack/horizon/openstack_dashboard/dashboards/project/dashboard.py' + Add line with windc project: + + ... +class BasePanels(horizon.PanelGroup): + slug = "compute" + name = _("Manage Compute") + panels = ('overview', + 'instances', + 'volumes', + 'images_and_snapshots', + 'access_and_security', + 'networks', + 'routers', + 'windc') + + ... + + 5. Run the test Django server: + cd /opt/stack/horizon + python manage.py runserver 67.207.197.36:8080 \ No newline at end of file diff --git a/dashboard/api/windc.py b/dashboard/api/windc.py new file mode 100644 index 0000000..e0407eb --- /dev/null +++ b/dashboard/api/windc.py @@ -0,0 +1,66 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import urlparse + +from django.utils.decorators import available_attrs +from windcclient.v1 import client as windc_client + + +__all__ = ('datacenter_get','datacenter_list', + 'datacenter_create','datacenter_delete') + + +LOG = logging.getLogger(__name__) + + +def windcclient(request): + o = urlparse.urlparse("http://127.0.0.1:8082") + url = "http://127.0.0.1:8082/foo" + LOG.debug('windcclient connection created using token "%s" and url "%s"' + % (request.user.token, url)) + return windc_client.Client(endpoint=url, token=None) + +def datacenters_create(request, parameters): + name = parameters.get('name', '') + return windcclient(request).datacenters.create(name) + +def datacenters_delete(request, datacenter_id): + return windcclient(request).datacenters.delete(datacenter_id) + +def datacenters_get(request, datacenter_id): + return windcclient(request).datacenters.get(datacenter_id) + +def datacenters_list(request): + return windcclient(request).datacenters.list() + +def services_create(request, datacenter, parameters): + name = parameters.get('name', '') + return windcclient(request).services.create(datacenter, name) + +def services_list(request, datacenter): + return windcclient(request).services.list(datacenter) + +def services_get(request, datacenter, service_id): + return windcclient(request).services.get(datacenter, service_id) + +def services_delete(request, datacenter, service_id): + return windcclient(request).services.delete(datacenter, service_id) diff --git a/dashboard/windc/__init__.py b/dashboard/windc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/windc/forms.py b/dashboard/windc/forms.py new file mode 100644 index 0000000..7c1329f --- /dev/null +++ b/dashboard/windc/forms.py @@ -0,0 +1,52 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from openstack_dashboard import api + +from horizon import exceptions +from horizon import forms +from horizon import messages + + +LOG = logging.getLogger(__name__) + + +class UpdateWinDC(forms.SelfHandlingForm): + tenant_id = forms.CharField(widget=forms.HiddenInput) + data_center = forms.CharField(widget=forms.HiddenInput) + name = forms.CharField(required=True) + + def handle(self, request, data): + try: + server = api.nova.server_update(request, data['data_center'], + data['name']) + messages.success(request, + _('Data Center "%s" updated.') % data['name']) + return server + except: + redirect = reverse("horizon:project:windc:index") + exceptions.handle(request, + _('Unable to update data center.'), + redirect=redirect) diff --git a/dashboard/windc/panel.py b/dashboard/windc/panel.py new file mode 100644 index 0000000..0731194 --- /dev/null +++ b/dashboard/windc/panel.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from openstack_dashboard.dashboards.project import dashboard + + +class WinDC(horizon.Panel): + name = _("Windows Data Centers") + slug = 'windc' + + +dashboard.Project.register(WinDC) diff --git a/dashboard/windc/tables.py b/dashboard/windc/tables.py new file mode 100644 index 0000000..8b24622 --- /dev/null +++ b/dashboard/windc/tables.py @@ -0,0 +1,140 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +# TO DO: clear extra modules + + +import logging + +from django import shortcuts +from django import template +from django.core import urlresolvers +from django.template.defaultfilters import title +from django.utils.http import urlencode +from django.utils.translation import string_concat, ugettext_lazy as _ + +from horizon.conf import HORIZON_CONFIG +from horizon import exceptions +from horizon import messages +from horizon import tables +from horizon.templatetags import sizeformat +from horizon.utils.filters import replace_underscores + +from openstack_dashboard import api +from openstack_dashboard.dashboards.project.access_and_security \ + .floating_ips.workflows import IPAssociationWorkflow + + +LOG = logging.getLogger(__name__) + + +class CreateService(tables.LinkAction): + name = "CreateService" + verbose_name = _("Create Service") + url = "horizon:project:windc:create" + classes = ("btn-launch", "ajax-modal") + + def allowed(self, request, datum): + return True + + def action(self, request, service): + # FIX ME + api.windc.services_create(request, service) + + +class CreateDataCenter(tables.LinkAction): + name = "CreateDataCenter" + verbose_name = _("Create Windows Data Center") + url = "horizon:project:windc:create_dc" + classes = ("btn-launch", "ajax-modal") + + def allowed(self, request, datum): + return True + + def action(self, request, datacenter): + api.windc.datacenters_create(request, datacenter) + + +class DeleteDataCenter(tables.BatchAction): + name = "delete" + action_present = _("Delete") + action_past = _("Delete") + data_type_singular = _("Data Center") + data_type_plural = _("Data Center") + classes = ('btn-danger', 'btn-terminate') + + def allowed(self, request, datum): + return True + + def action(self, request, datacenter_id): + datacenter = api.windc.datacenters_get(request, datacenter_id) + api.windc.datacenters_delete(request, datacenter) + + +class EditService(tables.LinkAction): + name = "edit" + verbose_name = _("Edit Service") + url = "horizon:project:windc:update" + classes = ("ajax-modal", "btn-edit") + + def allowed(self, request, instance): + return True + + +class ShowDataCenterServices(tables.LinkAction): + name = "edit" + verbose_name = _("Services") + url = "horizon:project:windc:services" + + def allowed(self, request, instance): + return True + + +class UpdateRow(tables.Row): + ajax = True + + def get_data(self, request, instance_id): + instance = api.nova.server_get(request, instance_id) + instance.full_flavor = api.nova.flavor_get(request, + instance.flavor["id"]) + return instance + + +class WinDCTable(tables.DataTable): + name = tables.Column("name", + link=("horizon:project:windc:services"), + verbose_name=_("Name")) + + class Meta: + name = "windc" + verbose_name = _("Windows Data Centers") + row_class = UpdateRow + table_actions = (CreateDataCenter,) + row_actions = (ShowDataCenterServices,DeleteDataCenter) + + +class WinServicesTable(tables.DataTable): + name = tables.Column("name", + link=("horizon:project:windc"), + verbose_name=_("Name")) + + class Meta: + name = "services" + verbose_name = _("Services") + row_class = UpdateRow + table_actions = (CreateService,) + row_actions = (EditService,) diff --git a/dashboard/windc/tabs.py b/dashboard/windc/tabs.py new file mode 100644 index 0000000..95b7217 --- /dev/null +++ b/dashboard/windc/tabs.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tabs + +from openstack_dashboard import api + + +class OverviewTab(tabs.Tab): + name = _("Services") + slug = "_services" + template_name = ("project/windc/_services.html") + + def get_context_data(self, request): + dc = self.tab_group.kwargs['domain_controller'] + return {"domain_controller": dc} + + +class WinServicesTab(tabs.TabGroup): + slug = "services_details" + tabs = (OverviewTab,) + sticky = True diff --git a/dashboard/windc/templates/windc/_data_center_help.html b/dashboard/windc/templates/windc/_data_center_help.html new file mode 100644 index 0000000..68ffe5a --- /dev/null +++ b/dashboard/windc/templates/windc/_data_center_help.html @@ -0,0 +1,2 @@ +{% load i18n %} +

{% blocktrans %}Data Center is an instance with different services.{% endblocktrans %}

\ No newline at end of file diff --git a/dashboard/windc/templates/windc/_dc_help.html b/dashboard/windc/templates/windc/_dc_help.html new file mode 100644 index 0000000..1cb4efc --- /dev/null +++ b/dashboard/windc/templates/windc/_dc_help.html @@ -0,0 +1,2 @@ +{% load i18n %} +

{% blocktrans %}You can deploy few domain controllers with one name.{% endblocktrans %}

\ No newline at end of file diff --git a/dashboard/windc/templates/windc/_iis_help.html b/dashboard/windc/templates/windc/_iis_help.html new file mode 100644 index 0000000..e8004c8 --- /dev/null +++ b/dashboard/windc/templates/windc/_iis_help.html @@ -0,0 +1,2 @@ +{% load i18n %} +

{% blocktrans %}You can deploy few Internet Information Services in one domain.{% endblocktrans %}

\ No newline at end of file diff --git a/dashboard/windc/templates/windc/_services.html b/dashboard/windc/templates/windc/_services.html new file mode 100644 index 0000000..1869508 --- /dev/null +++ b/dashboard/windc/templates/windc/_services.html @@ -0,0 +1,3 @@ +{% load i18n sizeformat %} + +

{% trans "Services" %}

\ No newline at end of file diff --git a/dashboard/windc/templates/windc/create.html b/dashboard/windc/templates/windc/create.html new file mode 100644 index 0000000..0508b68 --- /dev/null +++ b/dashboard/windc/templates/windc/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Windows Service" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Windows Service") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/dashboard/windc/templates/windc/create_dc.html b/dashboard/windc/templates/windc/create_dc.html new file mode 100644 index 0000000..2fc5894 --- /dev/null +++ b/dashboard/windc/templates/windc/create_dc.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Windows Data Center" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Windows Data Center") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/dashboard/windc/templates/windc/index.html b/dashboard/windc/templates/windc/index.html new file mode 100644 index 0000000..fa172ab --- /dev/null +++ b/dashboard/windc/templates/windc/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Windows Data Centers" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Windows Data Centers") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/dashboard/windc/templates/windc/services.html b/dashboard/windc/templates/windc/services.html new file mode 100644 index 0000000..2c7af81 --- /dev/null +++ b/dashboard/windc/templates/windc/services.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Data Center Services" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title="Data Center "|add:dc_name %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/dashboard/windc/templates/windc/services_tabs.html b/dashboard/windc/templates/windc/services_tabs.html new file mode 100644 index 0000000..8630ff8 --- /dev/null +++ b/dashboard/windc/templates/windc/services_tabs.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load i18n sizeformat %} +{% block title %}{% trans "Services" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title="Domain Controller Services" %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} \ No newline at end of file diff --git a/dashboard/windc/templates/windc/update.html b/dashboard/windc/templates/windc/update.html new file mode 100644 index 0000000..aba3dc9 --- /dev/null +++ b/dashboard/windc/templates/windc/update.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Instance" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Instance") %} +{% endblock page_header %} + +{% block main %} + {% include 'project/instances/_update.html' %} +{% endblock %} diff --git a/dashboard/windc/urls.py b/dashboard/windc/urls.py new file mode 100644 index 0000000..6654ed2 --- /dev/null +++ b/dashboard/windc/urls.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + +from .views import IndexView, CreateWinDCView, WinServices, CreateWinServiceView + + +VIEW_MOD = 'openstack_dashboard.dashboards.project.windc.views' + +urlpatterns = patterns(VIEW_MOD, + url(r'^$', IndexView.as_view(), name='index'), + url(r'^create$', CreateWinServiceView.as_view(), name='create'), + url(r'^create_dc$', CreateWinDCView.as_view(), name='create_dc'), + url(r'^(?P[^/]+)/$', WinServices.as_view(), + name='services') +) diff --git a/dashboard/windc/views.py b/dashboard/windc/views.py new file mode 100644 index 0000000..9c34c29 --- /dev/null +++ b/dashboard/windc/views.py @@ -0,0 +1,102 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations# under the License. + +""" +Views for managing instances. +""" +import logging + +from django import http +from django import shortcuts +from django.core.urlresolvers import reverse, reverse_lazy +from django.utils.datastructures import SortedDict +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tabs +from horizon import tables +from horizon import workflows + +from openstack_dashboard import api +from .tables import WinDCTable, WinServicesTable +from .workflows import CreateWinService, CreateWinDC + + +LOG = logging.getLogger(__name__) + + +class IndexView(tables.DataTableView): + table_class = WinDCTable + template_name = 'project/windc/index.html' + + def get_data(self): + # Gather our datacenters + try: + data_centers = api.windc.datacenters_list(self.request) + except: + data_centers = [] + exceptions.handle(self.request, + _('Unable to retrieve data centers list.')) + return data_centers + + +class WinServices(tables.DataTableView): + table_class = WinServicesTable + template_name = 'project/windc/services.html' + + def get_context_data(self, **kwargs): + context = super(WinServices, self).get_context_data(**kwargs) + data = self.get_data() + context["dc_name"] = self.dc_name + return context + + def get_data(self): + try: + dc_id = self.kwargs['domain_controller_id'] + datacenter = api.windc.datacenters_get(self.request, dc_id) + self.dc_name = datacenter.name + services = api.windc.services_list(self.request, datacenter) + except: + services = [] + exceptions.handle(self.request, + _('Unable to retrieve list of services for ' + 'data center "%s".') % dc_id) + return services + + +class CreateWinDCView(workflows.WorkflowView): + workflow_class = CreateWinDC + template_name = "project/windc/create_dc.html" + + def get_initial(self): + initial = super(CreateWinDCView, self).get_initial() + initial['project_id'] = self.request.user.tenant_id + initial['user_id'] = self.request.user.id + return initial + +class CreateWinServiceView(workflows.WorkflowView): + workflow_class = CreateWinService + template_name = "project/windc/create.html" + + def get_initial(self): + initial = super(CreateWinServiceView, self).get_initial() + initial['project_id'] = self.request.user.tenant_id + initial['user_id'] = self.request.user.id + return initial diff --git a/dashboard/windc/workflows.py b/dashboard/windc/workflows.py new file mode 100644 index 0000000..9724aa8 --- /dev/null +++ b/dashboard/windc/workflows.py @@ -0,0 +1,188 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import logging + +from django.utils.text import normalize_newlines +from django.utils.translation import ugettext as _ + +from horizon import exceptions +from horizon import forms +from horizon import workflows + +from openstack_dashboard import api + + +LOG = logging.getLogger(__name__) + + +class SelectProjectUserAction(workflows.Action): + project_id = forms.ChoiceField(label=_("Project")) + user_id = forms.ChoiceField(label=_("User")) + + def __init__(self, request, *args, **kwargs): + super(SelectProjectUserAction, self).__init__(request, *args, **kwargs) + # Set our project choices + projects = [(tenant.id, tenant.name) + for tenant in request.user.authorized_tenants] + self.fields['project_id'].choices = projects + + # Set our user options + users = [(request.user.id, request.user.username)] + self.fields['user_id'].choices = users + + class Meta: + name = _("Project & User") + # Unusable permission so this is always hidden. However, we + # keep this step in the workflow for validation/verification purposes. + permissions = ("!",) + + +class SelectProjectUser(workflows.Step): + action_class = SelectProjectUserAction + + +class ConfigureDCAction(workflows.Action): + name = forms.CharField(label=_("Data Center Name"), + required=True) + + class Meta: + name = _("Data Center") + help_text_template = ("project/windc/_data_center_help.html") + + +class ConfigureDC(workflows.Step): + action_class = ConfigureDCAction + contibutes = ("name",) + + def contribute(self, data, context): + if data: + context['name'] = data.get("name", "") + return context + + +class ConfigureWinDCAction(workflows.Action): + dc_name = forms.CharField(label=_("Domain Name"), + required=False) + + dc_net_name = forms.CharField(label=_("Domain NetBIOS Name"), + required=False, + help_text=_("A NetBIOS name of new domain.")) + + dc_count = forms.IntegerField(label=_("Domain Controllers Count"), + required=True, + min_value=1, + max_value=100, + initial=1) + + adm_password = forms.CharField(widget=forms.PasswordInput, + label=_("Administrator password"), + required=False, + help_text=_("Password for " + "administrator account.")) + + recovery_password = forms.CharField(widget=forms.PasswordInput, + label=_("Recovery password"), + required=False, + help_text=_("Password for " + "Active Directory " + "Recovery Mode.")) + + class Meta: + name = _("Domain Controllers") + help_text_template = ("project/windc/_dc_help.html") + + +class ConfigureWinDC(workflows.Step): + action_class = ConfigureWinDCAction + + +class ConfigureWinIISAction(workflows.Action): + iis_name = forms.CharField(label=_("IIS Server Name"), + required=False) + + iis_count = forms.IntegerField(label=_("IIS Servers Count"), + required=True, + min_value=1, + max_value=100, + initial=1) + + iis_domain = forms.CharField(label=_("Member of the Domain"), + required=False, + help_text=_("A name of domain for" + " IIS Server.")) + + class Meta: + name = _("Internet Information Services") + help_text_template = ("project/windc/_iis_help.html") + + +class ConfigureWinIIS(workflows.Step): + action_class = ConfigureWinIISAction + + +class CreateWinService(workflows.Workflow): + slug = "create" + name = _("Create Service") + finalize_button_name = _("Deploy") + success_message = _('Created service "%s".') + failure_message = _('Unable to create service "%s".') + success_url = "horizon:project:windc:services" + default_steps = (SelectProjectUser, + ConfigureWinDC, + ConfigureWinIIS) + + def format_status_message(self, message): + name = self.context.get('name', 'noname') + return message % name + + def handle(self, request, context): + try: + datacenter = context.get('domain_controller_name', '') + service = api.windc.services_create(request, context) + return True + except: + exceptions.handle(request) + return False + + + +class CreateWinDC(workflows.Workflow): + slug = "create" + name = _("Create Windows Data Center") + finalize_button_name = _("Create") + success_message = _('Created data center "%s".') + failure_message = _('Unable to create data center "%s".') + success_url = "horizon:project:windc:index" + default_steps = (SelectProjectUser, + ConfigureDC) + + def format_status_message(self, message): + name = self.context.get('name', 'noname') + return message % name + + def handle(self, request, context): + try: + datacenter = api.windc.datacenters_create(request, context) + return True + except: + exceptions.handle(request) + return False diff --git a/dashboard/windcclient/__init__.py b/dashboard/windcclient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/windcclient/common/__init__.py b/dashboard/windcclient/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/windcclient/common/base.py b/dashboard/windcclient/common/base.py new file mode 100644 index 0000000..9f03504 --- /dev/null +++ b/dashboard/windcclient/common/base.py @@ -0,0 +1,137 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Base utilities to build API operation managers and objects on top of. +""" + + +def getid(obj): + """ + Abstracts the common pattern of allowing both an object or an object's ID + (UUID) as a parameter when dealing with relationships. + """ + try: + return obj.id + except AttributeError: + return obj + + +class Manager(object): + """ + Managers interact with a particular type of API and provide CRUD + operations for them. + """ + resource_class = None + + def __init__(self, api): + self.api = api + + def _list(self, url, response_key, obj_class=None, body=None): + resp, body = self.api.client.json_request('GET', url, body=body) + + if obj_class is None: + obj_class = self.resource_class + + data = body[response_key] + return [obj_class(self, res, loaded=True) for res in data if res] + + def _delete(self, url): + self.api.client.raw_request('DELETE', url) + + def _update(self, url, body, response_key=None): + resp, body = self.api.client.json_request('PUT', url, body=body) + # PUT requests may not return a body + if body: + return self.resource_class(self, body[response_key]) + + def _create(self, url, body, response_key, return_raw=False): + resp, body = self.api.client.json_request('POST', url, body=body) + if return_raw: + return body[response_key] + return self.resource_class(self, body[response_key]) + + def _get(self, url, response_key, return_raw=False): + resp, body = self.api.client.json_request('GET', url) + if return_raw: + return body[response_key] + return self.resource_class(self, body[response_key]) + + +class Resource(object): + """ + A resource represents a particular instance of an object (tenant, user, + etc). This is pretty much just a bag for attributes. + + :param manager: Manager object + :param info: dictionary representing resource attributes + :param loaded: prevent lazy-loading if set to True + """ + def __init__(self, manager, info, loaded=False): + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + + def _add_details(self, info): + for (k, v) in info.iteritems(): + setattr(self, k, v) + + def __getattr__(self, k): + if k not in self.__dict__: + #NOTE(bcwaldon): disallow lazy-loading if already loaded once + if not self.is_loaded(): + self.get() + return self.__getattr__(k) + + raise AttributeError(k) + else: + return self.__dict__[k] + + def __repr__(self): + reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and + k != 'manager') + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + def get_info(self): + if not self.is_loaded(): + self.get() + if self._info: + return self._info.copy() + return {} + + def get(self): + # set_loaded() first ... so if we have to bail, we know we tried. + self.set_loaded(True) + if not hasattr(self.manager, 'get'): + return + + new = self.manager.get(self.id) + if new: + self._info = new._info + self._add_details(new._info) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + if hasattr(self, 'id') and hasattr(other, 'id'): + return self.id == other.id + return self._info == other._info + + def is_loaded(self): + return self._loaded + + def set_loaded(self, val): + self._loaded = val diff --git a/dashboard/windcclient/common/client.py b/dashboard/windcclient/common/client.py new file mode 100644 index 0000000..d8c9eb7 --- /dev/null +++ b/dashboard/windcclient/common/client.py @@ -0,0 +1,151 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +""" +OpenStack Client interface. Handles the REST calls and responses. +""" + +import httplib2 +import copy +import logging +import json + +from . import exceptions +from . import utils +from .service_catalog import ServiceCatalog + + +logger = logging.getLogger(__name__) + + +class HTTPClient(httplib2.Http): + + USER_AGENT = 'python-balancerclient' + + def __init__(self, endpoint=None, token=None, username=None, + password=None, tenant_name=None, tenant_id=None, + region_name=None, auth_url=None, auth_tenant_id=None, + timeout=600, insecure=False): + super(HTTPClient, self).__init__(timeout=timeout) + self.endpoint = endpoint + self.auth_token = token + self.auth_url = auth_url + self.auth_tenant_id = auth_tenant_id + self.username = username + self.password = password + self.tenant_name = tenant_name + self.tenant_id = tenant_id + self.region_name = region_name + self.force_exception_to_status_code = True + self.disable_ssl_certificate_validation = insecure + if self.endpoint is None: + self.authenticate() + + def _http_request(self, url, method, **kwargs): + """ Send an http request with the specified characteristics. + """ + + kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) + kwargs['headers'].setdefault('User-Agent', self.USER_AGENT) + if self.auth_token: + kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) + + resp, body = super(HTTPClient, self).request(url, method, **kwargs) + + if logger.isEnabledFor(logging.DEBUG): + utils.http_log(logger, (url, method,), kwargs, resp, body) + + if resp.status in (301, 302, 305): + return self._http_request(resp['location'], method, **kwargs) + + return resp, body + + def _json_request(self, method, url, **kwargs): + """ Wrapper around _http_request to handle setting headers, + JSON enconding/decoding and error handling. + """ + + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', 'application/json') + + if 'body' in kwargs and kwargs['body'] is not None: + kwargs['body'] = json.dumps(kwargs['body']) + + resp, body = self._http_request(url, method, **kwargs) + + if body: + try: + body = json.loads(body) + except ValueError: + logger.debug("Could not decode JSON from body: %s" % body) + else: + logger.debug("No body was returned.") + body = None + + if 400 <= resp.status < 600: + # DELETE THIS STRING + logger.exception(url) + raise exceptions.from_response(resp, body) + + return resp, body + + def raw_request(self, method, url, **kwargs): + url = self.endpoint + url + + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', + 'application/octet-stream') + + resp, body = self._http_request(url, method, **kwargs) + + if 400 <= resp.status < 600: + logger.exception(url) + raise exceptions.from_response(resp, body) + + return resp, body + + def json_request(self, method, url, **kwargs): + url = self.endpoint + url + resp, body = self._json_request(method, url, **kwargs) + return resp, body + + def authenticate(self): + token_url = self.auth_url + "/tokens" + body = {'auth': {'passwordCredentials': {'username': self.username, + 'password': self.password}}} + if self.tenant_id: + body['auth']['tenantId'] = self.tenant_id + elif self.tenant_name: + body['auth']['tenantName'] = self.tenant_name + + tmp_follow_all_redirects = self.follow_all_redirects + self.follow_all_redirects = True + try: + resp, body = self._json_request('POST', token_url, body=body) + finally: + self.follow_all_redirects = tmp_follow_all_redirects + + try: + self.service_catalog = ServiceCatalog(body['access']) + token = self.service_catalog.get_token() + self.auth_token = token['id'] + self.auth_tenant_id = token['tenant_id'] + except KeyError: + logger.exception("Parse service catalog failed.") + raise exceptions.AuthorizationFailure() + + self.endpoint = self.service_catalog.url_for(attr='region', + filter_value=self.region_name) diff --git a/dashboard/windcclient/common/exceptions.py b/dashboard/windcclient/common/exceptions.py new file mode 100644 index 0000000..4d17b8d --- /dev/null +++ b/dashboard/windcclient/common/exceptions.py @@ -0,0 +1,140 @@ +# Copyright 2010 Jacob Kaplan-Moss +""" +Exception definitions. +""" + + +class UnsupportedVersion(Exception): + """Indicates that the user is trying to use an unsupported + version of the API""" + pass + + +class CommandError(Exception): + pass + + +class AuthorizationFailure(Exception): + pass + + +class NoUniqueMatch(Exception): + pass + + +class NoTokenLookupException(Exception): + """This form of authentication does not support looking up + endpoints from an existing token.""" + pass + + +class EndpointNotFound(Exception): + """Could not find Service or Region in Service Catalog.""" + pass + + +class AmbiguousEndpoints(Exception): + """Found more than one matching endpoint in Service Catalog.""" + def __init__(self, endpoints=None): + self.endpoints = endpoints + + def __str__(self): + return "AmbiguousEndpoints: %s" % repr(self.endpoints) + + +class ClientException(Exception): + """ + The base exception class for all exceptions this library raises. + """ + def __init__(self, code, message=None, details=None): + self.code = code + self.message = message or self.__class__.message + self.details = details + + def __str__(self): + return "%s (HTTP %s)" % (self.message, self.code) + + +class BadRequest(ClientException): + """ + HTTP 400 - Bad request: you sent some malformed data. + """ + http_status = 400 + message = "Bad request" + + +class Unauthorized(ClientException): + """ + HTTP 401 - Unauthorized: bad credentials. + """ + http_status = 401 + message = "Unauthorized" + + +class Forbidden(ClientException): + """ + HTTP 403 - Forbidden: your credentials don't give you access to this + resource. + """ + http_status = 403 + message = "Forbidden" + + +class NotFound(ClientException): + """ + HTTP 404 - Not found + """ + http_status = 404 + message = "Not found" + + +class OverLimit(ClientException): + """ + HTTP 413 - Over limit: you're over the API limits for this time period. + """ + http_status = 413 + message = "Over limit" + + +# NotImplemented is a python keyword. +class HTTPNotImplemented(ClientException): + """ + HTTP 501 - Not Implemented: the server does not support this operation. + """ + http_status = 501 + message = "Not Implemented" + + +# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() +# so we can do this: +# _code_map = dict((c.http_status, c) +# for c in ClientException.__subclasses__()) +# +# Instead, we have to hardcode it: +_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, + Forbidden, NotFound, OverLimit, HTTPNotImplemented]) + + +def from_response(response, body): + """ + Return an instance of an ClientException or subclass + based on an httplib2 response. + + Usage:: + + resp, body = http.request(...) + if resp.status != 200: + raise exception_from_response(resp, body) + """ + cls = _code_map.get(response.status, ClientException) + if body: + if hasattr(body, 'keys'): + error = body[body.keys()[0]] + message = error.get('message', None) + details = error.get('details', None) + else: + message = 'n/a' + details = body + return cls(code=response.status, message=message, details=details) + else: + return cls(code=response.status) diff --git a/dashboard/windcclient/common/service_catalog.py b/dashboard/windcclient/common/service_catalog.py new file mode 100644 index 0000000..d2a91d6 --- /dev/null +++ b/dashboard/windcclient/common/service_catalog.py @@ -0,0 +1,62 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011, Piston Cloud Computing, Inc. +# Copyright 2011 Nebula, Inc. +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from . import exceptions + + +class ServiceCatalog(object): + """Helper methods for dealing with a Keystone Service Catalog.""" + + def __init__(self, resource_dict): + self.catalog = resource_dict + + def get_token(self): + """Fetch token details fron service catalog""" + token = {'id': self.catalog['token']['id'], + 'expires': self.catalog['token']['expires']} + try: + token['user_id'] = self.catalog['user']['id'] + token['tenant_id'] = self.catalog['token']['tenant']['id'] + except: + # just leave the tenant and user out if it doesn't exist + pass + return token + + def url_for(self, attr=None, filter_value=None, + service_type='loadbalancer', endpoint_type='publicURL'): + """Fetch an endpoint from the service catalog. + + Fetch the specified endpoint from the service catalog for + a particular endpoint attribute. If no attribute is given, return + the first endpoint of the specified type. + + See tests for a sample service catalog. + """ + catalog = self.catalog.get('serviceCatalog', []) + + for service in catalog: + if service['type'] != service_type: + continue + + endpoints = service['endpoints'] + for endpoint in endpoints: + if not filter_value or endpoint.get(attr) == filter_value: + return endpoint[endpoint_type] + + raise exceptions.EndpointNotFound('Endpoint not found.') diff --git a/dashboard/windcclient/common/utils.py b/dashboard/windcclient/common/utils.py new file mode 100644 index 0000000..cabcba8 --- /dev/null +++ b/dashboard/windcclient/common/utils.py @@ -0,0 +1,291 @@ +import os +import re +import sys +import uuid +import logging +import prettytable + +from . import exceptions + + +def arg(*args, **kwargs): + """Decorator for CLI args.""" + def _decorator(func): + add_arg(func, *args, **kwargs) + return func + return _decorator + + +def env(*vars, **kwargs): + """ + returns the first environment variable set + if none are non-empty, defaults to '' or keyword arg default + """ + for v in vars: + value = os.environ.get(v, None) + if value: + return value + return kwargs.get('default', '') + + +def add_arg(f, *args, **kwargs): + """Bind CLI arguments to a shell.py `do_foo` function.""" + + if not hasattr(f, 'arguments'): + f.arguments = [] + + # NOTE(sirp): avoid dups that can occur when the module is shared across + # tests. + if (args, kwargs) not in f.arguments: + # Because of the sematics of decorator composition if we just append + # to the options list positional options will appear to be backwards. + f.arguments.insert(0, (args, kwargs)) + + +def add_resource_manager_extra_kwargs_hook(f, hook): + """Adds hook to bind CLI arguments to ResourceManager calls. + + The `do_foo` calls in shell.py will receive CLI args and then in turn pass + them through to the ResourceManager. Before passing through the args, the + hooks registered here will be called, giving us a chance to add extra + kwargs (taken from the command-line) to what's passed to the + ResourceManager. + """ + if not hasattr(f, 'resource_manager_kwargs_hooks'): + f.resource_manager_kwargs_hooks = [] + + names = [h.__name__ for h in f.resource_manager_kwargs_hooks] + if hook.__name__ not in names: + f.resource_manager_kwargs_hooks.append(hook) + + +def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): + """Return extra_kwargs by calling resource manager kwargs hooks.""" + hooks = getattr(f, "resource_manager_kwargs_hooks", []) + extra_kwargs = {} + for hook in hooks: + hook_name = hook.__name__ + hook_kwargs = hook(args) + + conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) + if conflicting_keys and not allow_conflicts: + raise Exception("Hook '%(hook_name)s' is attempting to redefine" + " attributes '%(conflicting_keys)s'" % locals()) + + extra_kwargs.update(hook_kwargs) + + return extra_kwargs + + +def unauthenticated(f): + """ + Adds 'unauthenticated' attribute to decorated function. + Usage: + @unauthenticated + def mymethod(f): + ... + """ + f.unauthenticated = True + return f + + +def isunauthenticated(f): + """ + Checks to see if the function is marked as not requiring authentication + with the @unauthenticated decorator. Returns True if decorator is + set to True, False otherwise. + """ + return getattr(f, 'unauthenticated', False) + + +def service_type(stype): + """ + Adds 'service_type' attribute to decorated function. + Usage: + @service_type('volume') + def mymethod(f): + ... + """ + def inner(f): + f.service_type = stype + return f + return inner + + +def get_service_type(f): + """ + Retrieves service type from function + """ + return getattr(f, 'service_type', None) + + +def pretty_choice_list(l): + return ', '.join("'%s'" % i for i in l) + + +def print_list(objs, fields, formatters={}, sortby_index=0): + if sortby_index == None: + sortby = None + else: + sortby = fields[sortby_index] + + pt = prettytable.PrettyTable([f for f in fields], caching=False) + pt.align = 'l' + + for o in objs: + row = [] + for field in fields: + if field in formatters: + row.append(formatters[field](o)) + else: + field_name = field.lower().replace(' ', '_') + data = getattr(o, field_name, '') + row.append(data) + pt.add_row(row) + + print pt.get_string(sortby=sortby) + + +def print_flat_list(lst, field): + pt = prettytable.PrettyTable(field) + for el in lst: + pt.add_row([el]) + print pt.get_string() + + +def print_dict(d, property="Property"): + pt = prettytable.PrettyTable([property, 'Value'], caching=False) + pt.align = 'l' + [pt.add_row(list(r)) for r in d.iteritems()] + print pt.get_string(sortby=property) + + +def find_resource(manager, name_or_id): + """Helper for the _find_* methods.""" + # first try to get entity as integer id + try: + if isinstance(name_or_id, int) or name_or_id.isdigit(): + return manager.get(int(name_or_id)) + except exceptions.NotFound: + pass + + # now try to get entity as uuid + try: + uuid.UUID(str(name_or_id)) + return manager.get(name_or_id) + except (ValueError, exceptions.NotFound): + pass + + try: + try: + return manager.find(human_id=name_or_id) + except exceptions.NotFound: + pass + + # finally try to find entity by name + try: + return manager.find(name=name_or_id) + except exceptions.NotFound: + try: + # Volumes does not have name, but display_name + return manager.find(display_name=name_or_id) + except exceptions.NotFound: + msg = "No %s with a name or ID of '%s' exists." % \ + (manager.resource_class.__name__.lower(), name_or_id) + raise exceptions.CommandError(msg) + except exceptions.NoUniqueMatch: + msg = ("Multiple %s matches found for '%s', use an ID to be more" + " specific." % (manager.resource_class.__name__.lower(), + name_or_id)) + raise exceptions.CommandError(msg) + + +def _format_servers_list_networks(server): + output = [] + for (network, addresses) in server.networks.items(): + if len(addresses) == 0: + continue + addresses_csv = ', '.join(addresses) + group = "%s=%s" % (network, addresses_csv) + output.append(group) + + return '; '.join(output) + + +class HookableMixin(object): + """Mixin so classes can register and run hooks.""" + _hooks_map = {} + + @classmethod + def add_hook(cls, hook_type, hook_func): + if hook_type not in cls._hooks_map: + cls._hooks_map[hook_type] = [] + + cls._hooks_map[hook_type].append(hook_func) + + @classmethod + def run_hooks(cls, hook_type, *args, **kwargs): + hook_funcs = cls._hooks_map.get(hook_type) or [] + for hook_func in hook_funcs: + hook_func(*args, **kwargs) + + +def safe_issubclass(*args): + """Like issubclass, but will just return False if not a class.""" + + try: + if issubclass(*args): + return True + except TypeError: + pass + + return False + + +def import_class(import_str): + """Returns a class from a string including module and class.""" + mod_str, _sep, class_str = import_str.rpartition('.') + __import__(mod_str) + return getattr(sys.modules[mod_str], class_str) + +_slugify_strip_re = re.compile(r'[^\w\s-]') +_slugify_hyphenate_re = re.compile(r'[-\s]+') + + +# http://code.activestate.com/recipes/ +# 577257-slugify-make-a-string-usable-in-a-url-or-filename/ +def slugify(value): + """ + Normalizes string, converts to lowercase, removes non-alpha characters, + and converts spaces to hyphens. + + From Django's "django/template/defaultfilters.py". + """ + import unicodedata + if not isinstance(value, unicode): + value = unicode(value) + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') + value = unicode(_slugify_strip_re.sub('', value).strip().lower()) + return _slugify_hyphenate_re.sub('-', value) + + +def http_log(logger, args, kwargs, resp, body): +# if not logger.isEnabledFor(logging.DEBUG): +# return + + string_parts = ['curl -i'] + for element in args: + if element in ('GET', 'POST'): + string_parts.append(' -X %s' % element) + else: + string_parts.append(' %s' % element) + + for element in kwargs['headers']: + header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) + string_parts.append(header) + + logger.debug("REQ: %s\n" % "".join(string_parts)) + if 'body' in kwargs and kwargs['body']: + logger.debug("REQ BODY: %s\n" % (kwargs['body'])) + logger.debug("RESP:%s\n", resp) + logger.debug("RESP BODY:%s\n", body) diff --git a/dashboard/windcclient/shell.py b/dashboard/windcclient/shell.py new file mode 100644 index 0000000..196c7a7 --- /dev/null +++ b/dashboard/windcclient/shell.py @@ -0,0 +1,285 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Command-line interface to the OpenStack LBaaS API. +""" + +import argparse +import httplib2 +import os +import sys +import logging + +from balancerclient.common import exceptions as exc +from balancerclient.common import utils +from balancerclient.v1 import shell as shell_v1 + + +LOG = logging.getLogger(__name__) + + +class OpenStackBalancerShell(object): + + def get_base_parser(self): + parser = argparse.ArgumentParser( + prog='balancer', + description=__doc__.strip(), + epilog='See "balancer help COMMAND" ' + 'for help on a specific command.', + add_help=False, + formatter_class=OpenStackHelpFormatter, + ) + + # Global arguments + parser.add_argument('-h', + '--help', + action='store_true', + help=argparse.SUPPRESS) + + parser.add_argument('--debug', + default=False, + action='store_true', + help=argparse.SUPPRESS) + + parser.add_argument('--os_username', + metavar='', + default=utils.env('OS_USERNAME'), + help='Defaults to env[OS_USERNAME]') + + parser.add_argument('--os_password', + metavar='', + default=utils.env('OS_PASSWORD'), + help='Defaults to env[OS_PASSWORD]') + + parser.add_argument('--os_tenant_name', + metavar='', + default=utils.env('OS_TENANT_NAME'), + help='Defaults to env[OS_TENANT_NAME]') + + parser.add_argument('--os_tenant_id', + metavar='', + default=utils.env('OS_TENANT_ID'), + help='Defaults to env[OS_TENANT_ID]') + + parser.add_argument('--os_auth_url', + metavar='', + default=utils.env('OS_AUTH_URL'), + help='Defaults to env[OS_AUTH_URL]') + + parser.add_argument('--os_region_name', + metavar='', + default=utils.env('OS_REGION_NAME'), + help='Defaults to env[OS_REGION_NAME]') + + parser.add_argument('--os_balancer_api_version', + metavar='', + default=utils.env('OS_BALANCER_API_VERSION', + 'KEYSTONE_VERSION'), + help='Defaults to env[OS_BALANCER_API_VERSION]' + ' or 2.0') + + parser.add_argument('--token', + metavar='', + default=utils.env('SERVICE_TOKEN'), + help='Defaults to env[SERVICE_TOKEN]') + + parser.add_argument('--endpoint', + metavar='', + default=utils.env('SERVICE_ENDPOINT'), + help='Defaults to env[SERVICE_ENDPOINT]') + + return parser + + def get_subcommand_parser(self, version): + parser = self.get_base_parser() + + self.subcommands = {} + subparsers = parser.add_subparsers(metavar='') + + try: + actions_module = { + '1': shell_v1, + }[version] + except KeyError: + actions_module = shell_v1 + + self._find_actions(subparsers, actions_module) + self._find_actions(subparsers, self) + + return parser + + def _find_actions(self, subparsers, actions_module): + for attr in (a for a in dir(actions_module) if a.startswith('do_')): + # I prefer to be hypen-separated instead of underscores. + command = attr[3:].replace('_', '-') + callback = getattr(actions_module, attr) + desc = callback.__doc__ or '' + help = desc.strip().split('\n')[0] + arguments = getattr(callback, 'arguments', []) + + subparser = subparsers.add_parser( + command, + help=help, + description=desc, + add_help=False, + formatter_class=OpenStackHelpFormatter) + subparser.add_argument('-h', '--help', action='help', + help=argparse.SUPPRESS) + self.subcommands[command] = subparser + for (args, kwargs) in arguments: + subparser.add_argument(*args, **kwargs) + subparser.set_defaults(func=callback) + + def main(self, argv): + # Parse args once to find version + parser = self.get_base_parser() + (options, args) = parser.parse_known_args(argv) + + # build available subcommands based on version + api_version = options.os_balancer_api_version + subcommand_parser = self.get_subcommand_parser(api_version) + self.parser = subcommand_parser + + # Handle top-level --help/-h before attempting to parse + # a command off the command line + if not argv or options.help: + self.do_help(options) + return 0 + + # Parse args again and call whatever callback was selected + args = subcommand_parser.parse_args(argv) + + # Deal with global arguments + if args.debug: + httplib2.debuglevel = 1 + + # Short-circuit and deal with help command right away. + if args.func == self.do_help: + self.do_help(args) + return 0 + + #FIXME(usrleon): Here should be restrict for project id same as + # for username or apikey but for compatibility it is not. + + if not utils.isunauthenticated(args.func): + # if the user hasn't provided any auth data + if not (args.token or args.endpoint or args.os_username or + args.os_password or args.os_auth_url): + raise exc.CommandError('Expecting authentication method via \n' + ' either a service token, ' + '--token or env[SERVICE_TOKEN], \n' + ' or credentials, ' + '--os_username or env[OS_USERNAME].') + + # if it looks like the user wants to provide a service token + # but is missing something + if args.token or args.endpoint and not ( + args.token and args.endpoint): + if not args.token: + raise exc.CommandError( + 'Expecting a token provided via either --token or ' + 'env[SERVICE_TOKEN]') + + if not args.endpoint: + raise exc.CommandError( + 'Expecting an endpoint provided via either --endpoint ' + 'or env[SERVICE_ENDPOINT]') + + # if it looks like the user wants to provide a credentials + # but is missing something + if ((args.os_username or args.os_password or args.os_auth_url) + and not (args.os_username and args.os_password and + args.os_auth_url)): + if not args.os_username: + raise exc.CommandError( + 'Expecting a username provided via either ' + '--os_username or env[OS_USERNAME]') + + if not args.os_password: + raise exc.CommandError( + 'Expecting a password provided via either ' + '--os_password or env[OS_PASSWORD]') + + if not args.os_auth_url: + raise exc.CommandError( + 'Expecting an auth URL via either --os_auth_url or ' + 'env[OS_AUTH_URL]') + + if utils.isunauthenticated(args.func): + self.cs = shell_generic.CLIENT_CLASS(endpoint=args.os_auth_url) + else: + token = None + endpoint = None + if args.token and args.endpoint: + token = args.token + endpoint = args.endpoint + api_version = options.os_balancer_api_version + self.cs = self.get_api_class(api_version)( + username=args.os_username, + tenant_name=args.os_tenant_name, + tenant_id=args.os_tenant_id, + token=token, + endpoint=endpoint, + password=args.os_password, + auth_url=args.os_auth_url, + region_name=args.os_region_name) + + try: + args.func(self.cs, args) + except exc.Unauthorized: + raise exc.CommandError("Invalid OpenStack LBaaS credentials.") + except exc.AuthorizationFailure: + raise exc.CommandError("Unable to authorize user") + + def get_api_class(self, version): + try: + return { + "1": shell_v1.CLIENT_CLASS, + }[version] + except KeyError: + return shell_v1.CLIENT_CLASS + + @utils.arg('command', metavar='', nargs='?', + help='Display help for ') + def do_help(self, args): + """ + Display help about this program or one of its subcommands. + """ + if getattr(args, 'command', None): + if args.command in self.subcommands: + self.subcommands[args.command].print_help() + else: + raise exc.CommandError("'%s' is not a valid subcommand" % + args.command) + else: + self.parser.print_help() + + +# I'm picky about my shell help. +class OpenStackHelpFormatter(argparse.HelpFormatter): + def start_section(self, heading): + # Title-case the headings + heading = '%s%s' % (heading[0].upper(), heading[1:]) + super(OpenStackHelpFormatter, self).start_section(heading) + + +def main(): + try: + return OpenStackBalancerShell().main(sys.argv[1:]) + except Exception, err: + LOG.exception("The operation executed with an error %r." % err) + raise diff --git a/dashboard/windcclient/v1/__init__.py b/dashboard/windcclient/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/windcclient/v1/client.py b/dashboard/windcclient/v1/client.py new file mode 100644 index 0000000..3928773 --- /dev/null +++ b/dashboard/windcclient/v1/client.py @@ -0,0 +1,29 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +from windcclient.common import client +from . import datacenters +from . import services + + +class Client(object): + """Client for the WinDC v1 API.""" + + def __init__(self, **kwargs): + self.client = client.HTTPClient(**kwargs) + self.datacenters = datacenters.DCManager(self) + self.services = services.DCServiceManager(self) diff --git a/dashboard/windcclient/v1/datacenters.py b/dashboard/windcclient/v1/datacenters.py new file mode 100644 index 0000000..93aaef2 --- /dev/null +++ b/dashboard/windcclient/v1/datacenters.py @@ -0,0 +1,44 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +from windcclient.common import base + + +class DC(base.Resource): + """Represent load balancer device instance.""" + + def __repr__(self): + return "" % self._info + + +class DCManager(base.Manager): + resource_class = DC + + def list(self): + return self._list('/datacenters', 'datacenters') + + def create(self, name, **extra): + body = {'name': name, 'services': {}} + body.update(extra) + return self._create('/datacenters', body, 'datacenter') + + def delete(self, datacenter): + return self._delete("/datacenters/%s" % base.getid(datacenter)) + + def get(self, datacenter): + return self._get("/datacenters/%s" % base.getid(datacenter), + 'datacenter') diff --git a/dashboard/windcclient/v1/services.py b/dashboard/windcclient/v1/services.py new file mode 100644 index 0000000..3216f3c --- /dev/null +++ b/dashboard/windcclient/v1/services.py @@ -0,0 +1,48 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +from windcclient.common import base + + +class DCService(base.Resource): + def __repr__(self): + return "" % self._info + + +class DCServiceManager(base.Manager): + resource_class = DCService + + def list(self, datacenter): + return self._list('/datacenters/%s' % base.getid(datacenter), + 'services') + + def create(self, datacenter, name, **extra): + body = {'name': name,} + body.update(extra) + return self._create('/datacenters/%s' % base.getid(datacenter), + body, 'service') + + def delete(self, datacenter, service): + return self._delete("/datacenters/%s/%s" % \ + (base.getid(datacenter), + base.getid(service))) + + def get(self, datacenter, service): + return self._get("/datacenters/%s/%s" % \ + (base.getid(datacenter), + base.getid(service)), + 'service') diff --git a/windc/heat_run b/windc/heat_run new file mode 100755 index 0000000..69e4182 --- /dev/null +++ b/windc/heat_run @@ -0,0 +1,5 @@ +#!/bin/bash + +source openrc.sh +heat "$@" + diff --git a/windc/tests/manual/createServiceParameters b/windc/tests/manual/createServiceParameters index 6e9817c..0954f92 100644 --- a/windc/tests/manual/createServiceParameters +++ b/windc/tests/manual/createServiceParameters @@ -4,5 +4,5 @@ "domain": "ACME.cloud", "AdminUser": "Admin", "AdminPassword": "StrongPassword", -"DomainControllerNames": ["APP-AD001","APP-AD002"] +"DomainControllerNames": ["AD-DC001"] } diff --git a/windc/tools/pip-requires b/windc/tools/pip-requires index 0cb916b..d860a56 100644 --- a/windc/tools/pip-requires +++ b/windc/tools/pip-requires @@ -3,7 +3,7 @@ # package to get the right headers... greenlet>=0.3.1 -SQLAlchemy>=0.7 +SQLAlchemy<=0.7.9 anyjson eventlet>=0.9.12 PasteDeploy @@ -15,7 +15,7 @@ sqlalchemy-migrate>=0.7.2 httplib2 kombu iso8601>=0.1.4 - +PyChef # For paste.util.template used in keystone.common.template Paste diff --git a/windc/windc/adapters/openstack.py b/windc/windc/adapters/openstack.py new file mode 100644 index 0000000..9ca6733 --- /dev/null +++ b/windc/windc/adapters/openstack.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heatclient import Client + diff --git a/windc/windc/api/v1/datacenters.py b/windc/windc/api/v1/datacenters.py index 2b0e1f9..9e05062 100644 --- a/windc/windc/api/v1/datacenters.py +++ b/windc/windc/api/v1/datacenters.py @@ -26,24 +26,17 @@ from windc.db import api as db_api LOG = logging.getLogger(__name__) -class Controller(object): +class Datacenters_Controller(object): def __init__(self, conf): LOG.debug("Creating data centers controller with config:" "datacenters.py %s", conf) self.conf = conf - @utils.verify_tenant - def findLBforVM(self, req, tenant_id, vm_id): - LOG.debug("Got index request. Request: %s", req) - result = core_api.lb_find_for_vm(self.conf, tenant_id, vm_id) - return {'loadbalancers': result} - @utils.verify_tenant def index(self, req, tenant_id): LOG.debug("Got index request. Request: %s", req) result = core_api.dc_get_index(self.conf, tenant_id) LOG.debug("Got list of datacenters: %s", result) - result return {'datacenters': result} @utils.http_success_code(202) @@ -80,4 +73,4 @@ def create_resource(conf): """Datacenters resource factory method""" deserializer = wsgi.JSONRequestDeserializer() serializer = wsgi.JSONResponseSerializer() - return wsgi.Resource(Controller(conf), deserializer, serializer) \ No newline at end of file + return wsgi.Resource(Datacenters_Controller(conf), deserializer, serializer) diff --git a/windc/windc/api/v1/router.py b/windc/windc/api/v1/router.py index ed48ef1..79acc71 100644 --- a/windc/windc/api/v1/router.py +++ b/windc/windc/api/v1/router.py @@ -20,10 +20,6 @@ import routes from windc.api.v1 import datacenters from windc.api.v1 import services - -#from . import tasks - - from openstack.common import wsgi @@ -32,7 +28,7 @@ LOG = logging.getLogger(__name__) class API(wsgi.Router): - """WSGI router for balancer v1 API requests.""" + """WSGI router for windc v1 API requests.""" def __init__(self, conf, **local_conf): self.conf = conf @@ -41,16 +37,20 @@ class API(wsgi.Router): datacenter_resource = datacenters.create_resource(self.conf) datacenter_collection = tenant_mapper.collection( "datacenters", "datacenter", - controller=datacenter_resource, member_prefix="/{datacenter_id}", + controller=datacenter_resource, + member_prefix="/{datacenter_id}", formatted=False) service_resource = services.create_resource(self.conf) - service_collection = datacenter_collection.member.collection('services', 'service', - controller=service_resource, member_prefix="/{service_id}", - formatted=False) - service_collection.member.connect("/{status}", action="changeServiceStatus", - conditions={'method': ["PUT"]}) + service_collection = datacenter_collection.member.\ + collection('services','service', + controller=service_resource, + member_prefix="/{service_id}", + formatted=False) + service_collection.member.connect("/{status}", + action="changeServiceStatus", + conditions={'method': ["PUT"]}) mapper.connect("/servicetypes", - controller=datacenter_resource, - action="show_servicetypes", - conditions={'method': ["GET"]}) + controller=datacenter_resource, + action="show_servicetypes", + conditions={'method': ["GET"]}) super(API, self).__init__(mapper) diff --git a/windc/windc/api/v1/services.py b/windc/windc/api/v1/services.py index e635250..6040926 100644 --- a/windc/windc/api/v1/services.py +++ b/windc/windc/api/v1/services.py @@ -26,22 +26,17 @@ from windc.db import api as db_api LOG = logging.getLogger(__name__) -class Controller(object): +class Services_Controller(object): def __init__(self, conf): LOG.debug("Creating services controller with config:" "services.py %s", conf) self.conf = conf - @utils.verify_tenant - def findLBforVM(self, req, tenant_id, vm_id): - LOG.debug("Got index request. Request: %s", req) - result = core_api.lb_find_for_vm(self.conf, tenant_id, vm_id) - return {'loadbalancers': result} - @utils.verify_tenant def index(self, req, tenant_id, datacenter_id): LOG.debug("Got index request. Request: %s", req) - result = core_api.service_get_index(self.conf, tenant_id, datacenter_id) + result = core_api.service_get_index(self.conf, tenant_id, + datacenter_id) return {'services': result} @utils.http_success_code(202) @@ -61,19 +56,22 @@ class Controller(object): @utils.verify_tenant def delete(self, req, tenant_id, datacenter_id, service_id): LOG.debug("Got delete request. Request: %s", req) - core_api.delete_service(self.conf, tenant_id, datacenter_id, service_id) + core_api.delete_service(self.conf, tenant_id, + datacenter_id, service_id) @utils.verify_tenant def show(self, req, tenant_id, datacenter_id, service_id): LOG.debug("Got loadbalancerr info request. Request: %s", req) - result = core_api.service_get_data(self.conf, tenant_id, datacenter_id, service_id) + result = core_api.service_get_data(self.conf, tenant_id, + datacenter_id, service_id) return {'service': result} @utils.http_success_code(202) @utils.verify_tenant def update(self, req, tenant_id, datacenter_id, service_id, body): LOG.debug("Got update request. Request: %s", req) - core_api.update_service(self.conf, tenant_id, datacenter_id, service_id, body) + core_api.update_service(self.conf, tenant_id, datacenter_id, + service_id, body) return {'service': {'id': service_id}} @@ -81,7 +79,4 @@ def create_resource(conf): """Services resource factory method""" deserializer = wsgi.JSONRequestDeserializer() serializer = wsgi.JSONResponseSerializer() - return wsgi.Resource(Controller(conf), deserializer, serializer) - - - + return wsgi.Resource(Services_Controller(conf), deserializer, serializer) diff --git a/windc/windc/common/wsgi.py b/windc/windc/common/wsgi.py index 8d01d31..3f1c6b5 100644 --- a/windc/windc/common/wsgi.py +++ b/windc/windc/common/wsgi.py @@ -46,7 +46,7 @@ from windc.common import utils bind_opts = [ - cfg.StrOpt('bind_host', default='0.0.0.0'), + cfg.StrOpt('bind_host', default='localhost'), cfg.IntOpt('bind_port'), ] diff --git a/windc/windc/core/api.py b/windc/windc/core/api.py index 5c12a2b..c9d1160 100644 --- a/windc/windc/core/api.py +++ b/windc/windc/core/api.py @@ -51,19 +51,22 @@ def update_dc(conf, tenant_id, datacenter_id, body): old_dc = copy.deepcopy(dc) db_api.pack_update(dc, body) dc = db_api.datacenter_update(conf, datacenter_id, dc) - event = events.Event(events.SCOPE_DATACENTER_CHANGE, events.ACTION_MODIFY) + event = events.Event(events.SCOPE_DATACENTER_CHANGE, + events.ACTION_MODIFY) event.previous_state = old_dc events.change_event(conf, event, dc) pass def service_get_index(conf, tenant_id, datacenter_id): - srvcs = db_api.service_get_all_by_datacenter_id(conf, tenant_id, dtacenter_id) + srvcs = db_api.service_get_all_by_datacenter_id(conf, tenant_id, + datacenter_id) srv_list = [db_api.unpack_extra(srv) for srv in srvcs] return srv_list pass def create_service(conf, params): - # We need to pack all attributes which are not defined by the model explicitly + # We need to pack all attributes which are not defined + # by the model explicitly srv_params = db_api.service_pack_extra(params) srv = db_api.service_create(conf, srv_params) event = events.Event(events.SCOPE_SERVICE_CHANGE, events.ACTION_ADD) @@ -80,7 +83,7 @@ def delete_service(conf, tenant_id, datacenter_id, service_id): pass def service_get_data(conf, tenant_id, datacenter_id, service_id): - srv = db_api.service_get(conf,service_id, tenant_id) + srv = db_api.service_get(conf, service_id, tenant_id) srv_data = db_api.unpack_extra(srv) return srv_data pass @@ -93,4 +96,4 @@ def update_service(conf, tenant_id, datacenter_id, service_id, body): event = events.Event(events.SCOPE_SERVICE_CHANGE, events.ACTION_MODIFY) event.previous_state = old_srv events.change_event(conf, event, srv) - pass \ No newline at end of file + pass diff --git a/windc/windc/core/builder.py b/windc/windc/core/builder.py index 2dc68e8..15a14e6 100644 --- a/windc/windc/core/builder.py +++ b/windc/windc/core/builder.py @@ -29,5 +29,9 @@ class Builder: def build(self, context, event, data): pass +def create_context(): + context = {} + context['commands']=[] + return context diff --git a/windc/windc/core/builders/ActiveDirectory.py b/windc/windc/core/builders/ActiveDirectory.py index 7181329..703703b 100644 --- a/windc/windc/core/builders/ActiveDirectory.py +++ b/windc/windc/core/builders/ActiveDirectory.py @@ -17,11 +17,14 @@ import logging +import uuid LOG = logging.getLogger(__name__) from windc.core.builder import Builder from windc.core import change_events as events from windc.db import api as db_api +from windc.core.templates import Template +from windc.core import commands as command_api class ActiveDirectory(Builder): def __init__(self): @@ -35,6 +38,8 @@ class ActiveDirectory(Builder): LOG.info ("Got service change event. Analysing..") if self.do_analysis(context, event, dc): self.plan_changes(context, event, dc) + + self.submit_commands(context, event, dc) else: LOG.debug("Not in my scope. Skip event.") pass @@ -44,10 +49,66 @@ class ActiveDirectory(Builder): zones = data['zones'] if data['type'] == self.type and len(zones) == 1: LOG.debug("It is a service which I should build.") + datacenter_id = data['datacenter_id'] + dc = db_api.datacenter_get(context['conf'],data['tenant_id'], + data['datacenter_id']) + datacenter = db_api.unpack_extra(dc) + context['stack_name']=datacenter['name'] return True else: return False def plan_changes(self, context, event, data): + # Here we can plan multiple command execution. + # It might be Heat call command, then chef call command and other + # + LOG.debug("Plan changes...") + self.prepare_template(context, event, data) + self.chef_configuration(context, event, data) + context['commands'].append(self.deploy_template_command(context, event, data)) + context['commands'].append(self.chef_configuration_command(context, event, data)) pass + def prepare_template(self, context, event, data): + LOG.debug("Prepare CloudFormation Template...") + template = Template() + template.add_description('Base template for Active Directory deployment') + sec_grp = template.create_security_group('Security group for AD') + rule = template.create_securitygroup_rule('tcp','3389','3389','0.0.0.0/0') + template.add_rule_to_securitygroup(sec_grp, rule) + template.add_resource('ADSecurityGroup', sec_grp) + + instance = template.create_instance() + instance_name= 'AD-DC001' + template.add_security_group(instance, 'ADSecurityGroup') + template.add_resource(instance_name, instance) + + template.add_output_value(instance_name+'-IP',{"Fn::GetAtt" : [instance_name,'PublicIp']}, + 'Public IP for the domain controller.') + context['template']=template + pass + + def deploy_template_command(self, context, event, data): + LOG.debug("Creating CloudFormation Template deployment command...") + fname = "templates/"+str(uuid.uuid4()) + f=open(fname, "w") + f.write(context['template'].to_json()) + f.close() + context['template_name']=fname + command = command_api.Command(command_api.TEMPLATE_DEPLOYMENT_COMMAND, context) + return command + pass + + def chef_configuration(self, context, event, data): + LOG.debug("Creating Chef configuration...") + context['Role'] = 'pdc' + pass + + def chef_configuration_command(self, context, event, data): + LOG.debug("Creating Chef configuration command...") + command = command_api.Command(command_api.CHEF_COMMAND, context) + return command + + def submit_commands(self, context, event, data): + LOG.debug("Submit commands for execution...") + pass \ No newline at end of file diff --git a/windc/windc/core/change_events.py b/windc/windc/core/change_events.py index 8324a7f..8955b58 100644 --- a/windc/windc/core/change_events.py +++ b/windc/windc/core/change_events.py @@ -20,6 +20,8 @@ import logging LOG = logging.getLogger(__name__) from windc.core import builder_set +from windc.core import builder +from windc.drivers import command_executor #Declare events types SCOPE_SERVICE_CHANGE = "Service" @@ -40,11 +42,14 @@ class Event: def change_event(conf, event, data): LOG.info("Change event of type: %s ", event) - context = {} + context = builder.create_context() context['conf'] = conf for builder_type in builder_set.builders.set: - builder = builder_set.builders.set[builder_type] - builder.build(context, event, data) + builder_instance = builder_set.builders.set[builder_type] + builder_instance.build(context, event, data) + + executor = command_executor.Executor() + executor.execute(context['commands']) pass diff --git a/windc/windc/core/commands.py b/windc/windc/core/commands.py new file mode 100644 index 0000000..86473c1 --- /dev/null +++ b/windc/windc/core/commands.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +TEMPLATE_DEPLOYMENT_COMMAND = "Template" +CHEF_COMMAND = "Chef" +CHEF_OP_CREATE_ENV = "Env" +CHEF_OP_CREATE_ROLE = "Role" +CHEF_OP_ASSIGN_ROLE = "AssignRole" +CHEF_OP_CREATE_NODE = "CRNode" + +class Command: + type = "Empty" + context = None + + def __init__(self): + self.type = "Empty" + self.context = None + self.data = None + + def __init__(self, type, context): + self.type = type + self.context = context + + def __init__(self, type, context, data): + self.type = type + self.context = context + self.data = data + + diff --git a/windc/windc/core/templates.py b/windc/windc/core/templates.py new file mode 100644 index 0000000..47a7c90 --- /dev/null +++ b/windc/windc/core/templates.py @@ -0,0 +1,107 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import logging +from windc.common.wsgi import JSONResponseSerializer +LOG = logging.getLogger(__name__) + +class Template: + def __init__(self): + self.content = {'AWSTemplateFormatVersion':'2010-09-09', 'Description':'', + 'Parameters':{}} + self.content['Mappings'] = { + "AWSInstanceType2Arch" : { + "t1.micro" : { "Arch" : "32" }, + "m1.small" : { "Arch" : "32" }, + "m1.large" : { "Arch" : "64" }, + "m1.xlarge" : { "Arch" : "64" }, + "m2.xlarge" : { "Arch" : "64" }, + "m2.2xlarge" : { "Arch" : "64" }, + "m2.4xlarge" : { "Arch" : "64" }, + "c1.medium" : { "Arch" : "32" }, + "c1.xlarge" : { "Arch" : "64" }, + "cc1.4xlarge" : { "Arch" : "64" } + }, + "DistroArch2AMI": { + "F16" : { "32" : "F16-i386-cfntools", "64" : "F16-x86_64-cfntools" }, + "F17" : { "32" : "F17-i386-cfntools", "64" : "F17-x86_64-cfntools" }, + "U10" : { "32" : "U10-i386-cfntools", "64" : "U10-x86_64-cfntools" }, + "RHEL-6.1": { "32" : "rhel61-i386-cfntools", "64" : "rhel61-x86_64-cfntools" }, + "RHEL-6.2": { "32" : "rhel62-i386-cfntools", "64" : "rhel62-x86_64-cfntools" }, + "RHEL-6.3": { "32" : "rhel63-i386-cfntools", "64" : "rhel63-x86_64-cfntools" } + } + } + self.content['Resources'] = {} + self.content['Outputs'] = {} + + def to_json(self): + serializer = JSONResponseSerializer() + json = serializer.to_json(self.content) + return json + + + def empty_template(self): + pass + + def add_description(self, description): + self.content['Description'] = description + + def add_parameter(self, name, parameter): + self.content['Parameters'].update({name : parameter}) + + def add_resource(self, name, resource): + self.content['Resources'].update({name : resource}) + + def create_parameter(self, defult, type, decription): + parameter = {'Default':default, 'Type':type, 'Description':description} + return parameter + + def create_security_group(self, description): + sec_grp = {'Type':'AWS::EC2::SecurityGroup'} + sec_grp['Properties'] = {} + sec_grp['Properties']['GroupDescription'] = description + sec_grp['Properties']['SecurityGroupIngress'] = [] + return sec_grp + + def add_rule_to_securitygroup(self, grp, rule): + grp['Properties']['SecurityGroupIngress'].append(rule) + + def create_securitygroup_rule(self, proto, f_port, t_port, cidr): + rule = {'IpProtocol':proto, 'FromPort':f_port, 'ToPort':t_port,'CidrIp': cidr} + return rule + + def create_instance(self): + instance = {'Type':'AWS::EC2::Instance','Metadata':{},'Properties':{}} + instance['Properties']['ImageId'] = 'U10-x86_64-cfntools' + instance['Properties']['SecurityGroups']=[] + instance['Properties']['KeyName'] = 'keero-linux-keys' + instance['Properties']['InstanceType'] = 'm1.small' + return instance + + def add_security_group(self, instance, grp_name): + instance['Properties']['SecurityGroups'].append({'Ref': grp_name}) + + def add_output_value(self, name, value, description): + self.content['Outputs'].update({name:{'Value':value, 'Description':description}}) + + def get_content(self): + return self.content + + + + diff --git a/windc/windc/db/api.py b/windc/windc/db/api.py index d01c3ca..1e06f78 100644 --- a/windc/windc/db/api.py +++ b/windc/windc/db/api.py @@ -88,7 +88,8 @@ def datacenter_create(conf, values): def datacenter_update(conf, datacenter_id, values): session = get_session(conf) with session.begin(): - datacenter_ref = datacenter_get(conf, datacenter_id, session=session) + datacenter_ref = session.query(models.DataCenter).\ + filter_by(id=datacenter_id).first() datacenter_ref.update(values) return datacenter_ref @@ -96,8 +97,10 @@ def datacenter_update(conf, datacenter_id, values): def datacenter_destroy(conf, datacenter_id): session = get_session(conf) with session.begin(): - datacenter_ref = device_get(conf, datacenter_id, session=session) + datacenter_ref = session.query(models.DataCenter).\ + filter_by(id=datacenter_id).first() session.delete(datacenter_ref) + return datacenter_ref # Service @@ -112,31 +115,10 @@ def service_get(conf, service_id, tenant_id=None, session=None): raise exception.ServiceNotFound(service_ref=service_ref) return service_ref - -def service_get_all_by_project(conf, tenant_id): - session = get_session(conf) - query = session.query(models.Service).filter_by(tenant_id=tenant_id) - return query.all() - - -def service_get_all_by_vm_id(conf, tenant_id, vm_id): - session = get_session(conf) - query = session.query(models.Service).distinct().\ - filter_by(tenant_id=tenant_id).\ - filter(vm_id == vm_id) - return query.all() - - def service_get_all_by_datacenter_id(conf, tenant_id, datacenter_id): session = get_session(conf) query = session.query(models.Service).filter_by(datacenter_id=datacenter_id) - service_refs = query.all() - if not service_refs: - raise exception.ServiceNotFound('No service ' - 'for the datacenter %s found' - % datacenter_id) - return service_refs - + return query.all() def service_create(conf, values): session = get_session(conf) @@ -146,7 +128,6 @@ def service_create(conf, values): session.add(service_ref) return service_ref - def service_update(conf, service_id, values): session = get_session(conf) with session.begin(): @@ -155,13 +136,23 @@ def service_update(conf, service_id, values): service_ref['updated_at'] = datetime.datetime.utcnow() return service_ref - def service_destroy(conf, service_id): session = get_session(conf) with session.begin(): service_ref = service_get(conf, service_id, session=session) session.delete(service_ref) +def service_get_all_by_project(conf, tenant_id): + session = get_session(conf) + query = session.query(models.Service).filter_by(tenant_id=tenant_id) + return query.all() + +def service_get_all_by_vm_id(conf, tenant_id, vm_id): + session = get_session(conf) + query = session.query(models.Service).distinct().\ + filter_by(tenant_id=tenant_id).\ + filter(vm_id == vm_id) + return query.all() def service_count_active_by_datacenter(conf, datacenter_id): session = get_session(conf) @@ -171,5 +162,3 @@ def service_count_active_by_datacenter(conf, datacenter_id): filter_by(status=service_status.ACTIVE).\ count() return service_count - - diff --git a/windc/windc/drivers/command_executor.py b/windc/windc/drivers/command_executor.py new file mode 100644 index 0000000..c7c0d2f --- /dev/null +++ b/windc/windc/drivers/command_executor.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Piston Cloud Computing, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from windc.core import commands as commands_api +from windc.drivers import openstack_heat + +class Executor: + + map = {commands_api.TEMPLATE_DEPLOYMENT_COMMAND : openstack_heat.Heat} + + def __init__(self): + pass + + def execute(self, commands): + for command in commands: + if command.type == commands_api.TEMPLATE_DEPLOYMENT_COMMAND: + executor = openstack_heat.Heat() + executor.execute(command) + + diff --git a/windc/windc/drivers/openstack_heat.py b/windc/windc/drivers/openstack_heat.py new file mode 100644 index 0000000..7661bb6 --- /dev/null +++ b/windc/windc/drivers/openstack_heat.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Piston Cloud Computing, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +#from heatclient import Client +from subprocess import call + +import logging +LOG = logging.getLogger(__name__) + +class Heat: + + def __init__(self): + pass + + def execute(self, command): +# client = Client('1',OS_IMAGE_ENDPOINT, OS_TENANT_ID) + LOG.debug('Calling heat script to execute template') + call(["./heat_run","stack-create","-f "+command.context['template_name'], + command.context['stack_name']]) + pass +