Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first commit

  • Loading branch information...
commit f42e2b4ca1b4af99657c9f1b3ca1a22e6f91340b 0 parents
@ke4qqq ke4qqq authored
62 CHANGES.rdoc
@@ -0,0 +1,62 @@
+= Changes
+
+== 2011-11-03 (0.0.11)
+* Added LICENSE file and licensing headers to source. No feature changes.
+
+== 2011-10-13 (0.0.10)
+* Support for multi-homed VMs: The --networks option for the <tt>server create</tt> command takes a comma-separated list
+ of network offering names. The created VM will have one nic per network, with nics assigned to networks in the order
+ specified (e.g. the first network in the list will be assigned to eth0, the second to eth1 and so on). The --networks
+ option replaces the --network option, which supported a single network name.
+* The <tt>server list</tt> command now shows the VM 'displayname' in parentheses when the displayname is defined and is
+ different than the VM name.
+* Public IP logic has been updated to support multi-homed VMs. It now uses the first nic attached to a network with the
+ 'default' value set to true as the primary interface.
+
+== 2011-08-30 (0.0.9)
+* Added subcommands: <tt>server start</tt>, <tt>server stop</tt> and <tt>server reboot</tt>.
+* Updated the <tt>hosts</tt> command to return short hostnames and FQDNs. Also added comments where needed so the output
+ can be directly pasted into /etc/hosts.
+
+== 2011-08-06 (0.0.8)
+* Changed cloudstack async command timeout from 60 to 300 seconds.
+
+== 2011-07-21 (0.0.7)
+* Fixed http_request actions in <tt>stack create</tt>.
+
+== 2011-07-15 (0.0.6)
+* The <tt>server delete</tt> command now deletes the chef node and client if their names match the fqdn of the server.
+* Added the <tt>stack create</tt> and <tt>stack delete</tt> commands for bulk creation and deletion of servers using a
+ JSON definition file. These commands are still experimental and subject to change. See README.rdoc for details.
+* Added a <tt>hosts</tt> command, which lists the public ip address and fqdn of all servers in /etc/hosts file format.
+* Fixed nil reference bug in <tt>server list</tt> command.
+
+== 2011-06-05 (0.0.5)
+* Added <tt>--port-rules</tt> option to the <tt>cs server create</tt> command. It accepts a list of port forwarding
+ rules to be created for the server (only applies to servers on a virtual network). See 'Port forwarding rules for
+ virtual networks' in README.rdoc for details.
+
+== 2011-06-01 (0.0.4)
+* Fixed ssh detection bug.
+
+== 2011-05-30 (0.0.3)
+
+* Added support for virtual networks. A public IP address is allocated for each new server in a virtual network
+ and an ssh port forwarding rule is created. The IP is released when the server is destroyed as long as it doesn't
+ have forwarding rules for any servers other than the one being deleted.
+* Default network is now detected automatically. If there is more than one network marked as 'default' for an
+ account, the first Direct network is preferred.
+
+== 2011-05-22 (0.0.2)
+
+* Added subcommands: <tt>service list</tt>, <tt>template list</tt>, <tt>network list</tt> and <tt>zone list</tt>.
+* Removed -H (and --hostname) option from the <tt>cs server create</tt> command. The host name is now the first
+ argument to the command: <tt>knife cs server create myhostname</tt>. If a host name is not specified, CloudStack
+ will use an auto-generated name.
+* Added --no-bootstrap option to the server create command. This prevents Chef from being installed on the new server.
+* Fixed help banners (commands were shown as <tt>knife cloudstack ...</tt> instead of <tt>knife cs ...</tt>).
+* Added README.rdoc.
+
+== 2011-05-15 (0.0.1)
+
+* Initial release
202 LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
211 README.rdoc
@@ -0,0 +1,211 @@
+= knife-cloudstack
+
+== DESCRIPTION:
+
+This is the Edmunds Knife plugin for CloudStack. This plugin gives Knife the ability to create, bootstrap and manage
+CloudStack instances.
+
+== INSTALLATION:
+This plugin is distributed as a Ruby Gem. To install it, run:
+
+ gem install knife-cloudstack
+
+Depending on your system's configuration, you may need to run this command with root privileges.
+
+== CONFIGURATION:
+In order to communicate with the CloudStack API you will have to tell Knife about your CloudStack API Key, your Secret Key and the URL of the API endpoint.
+The easiest way to accomplish this is to create some entries in your <tt>knife.rb</tt> file:
+
+ knife[:cloudstack_url] = "http://yourcloudstackserver.com:8080/client/api
+ knife[:cloudstack_api_key] = "Your CloudStack API Key"
+ knife[:cloudstack_secret_key] = "Your CloudStack Secret Key"
+
+If your knife.rb file will be checked into a SCM system (ie readable by others) you may want to read the values from environment variables:
+
+ knife[:cloudstack_url] = "#{ENV['CLOUDSTACK_URL']}"
+ knife[:cloudstack_api_key] = "#{ENV['CLOUDSTACK_API_KEY']}"
+ knife[:cloudstack_secret_key] = "#{ENV['CLOUDSTACK_SECRET_KEY']}"
+
+You also have the option of passing your CloudStack URL and API/Secret Keys into the individual knife subcommands using the <tt>-U</tt> (or <tt>--cloudstack-url</tt>),
+<tt>-A</tt> (or <tt>--cloudstack-api-key </tt>) <tt>-K</tt> (or <tt>--cloudstack-secret-key</tt>) command options
+ # provision a new small RHEL 5.6 webserver
+ knife cs server create my-server -r 'role[webserver]' -S "small" -T "RHEL 5.6 Base" -A 'Your CloudStack API Key' -K "Your CloudStack Secret Key" --distro "rhel5-gems"
+
+Additionally the following options may be set in your <tt>knife.rb</tt>:
+
+* knife[:cloudstack_service]
+* knife[:cloudstack_template]
+* knife[:cloudstack_zone]
+* knife[:distro]
+* knife[:template_file]
+
+
+== SUBCOMMANDS:
+
+This plugin provides the following Knife subcommands. Specific command options can be found by invoking the subcommand with a <tt>--help</tt> flag
+
+=== knife cs server create
+
+Provisions a new server in CloudStack and then performs a Chef bootstrap (using the SSH protocol). The goal of the bootstrap is to get Chef installed on the target
+system so it can run Chef Client with a Chef Server. The main assumption is a baseline OS installation exists (provided by the provisioning). It is primarily
+intended for Chef Client systems that talk to a Chef server. By default the server is bootstrapped using the {ubuntu10.04-gems}[https://github.com/opscode/chef/bl
+ob/master/chef/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb] template. This can be overridden using the <tt>-d</tt> or <tt>--template-file</tt> command options.
+
+==== Port forwarding rules for virtual networks
+The <tt>--port-rules</tt> option takes a comma separated list of port forwarding rules. These rules are created on the
+virtual public ip address of the server. Note that this option only applies to servers using a virtual network; it is
+ignored if the server's public ip address is on a direct attached network.
+
+Port forwarding rules have the syntax <tt>PUBLIC_PORT[:PRIVATE_PORT[:PROTOCOL]]</tt>. <tt>PRIVATE_PORT</tt> and
+<tt>PROTOCOL</tt> are optional. The default value of <tt>PRIVATE_PORT</tt> is <tt>PUBLIC_PORT</tt> and the default
+<tt>PROTOCOL</tt> is 'TCP'.
+For example, a rule to forward from public port 80 to private port 7000 would look like <tt>80:7000:TCP</tt>.
+Since 'TCP' is the default protocol, the rule can be shortened to <tt>80:7000</tt>. A rule can even be shortened to a
+single number when the public and private ports are the same. For example, a rule to forward from public port 25 to
+private port 25 can be stated as simply <tt>25</tt>. A list of such rules for a webserver might look like
+<tt>80,443</tt>.
+
+=== knife cs server delete
+
+Deletes an existing server in the currently configured CloudStack account. <b>PLEASE NOTE</b> - this does not delete
+the associated node and client objects from the Chef server.
+
+=== knife cs server list
+
+Displays a list of all servers in the currently configured CloudStack account. <b>PLEASE NOTE</b> - this shows all
+servers associated with the cloudstack account including servers that are not registered with a Chef server.
+
+=== knife cs network list
+Displays a list of all networks available in the currently configured CloudStack account. A network can be specified
+when creating a new server by passing the network name as an argument to the -W (or --network) option of the
+<tt>knife cs server create</tt> command.
+
+=== knife cs service list
+Displays a list of all service offerings available in the currently configured CloudStack account. A service offering
+can be specified when creating a new server by passing the name as an argument to the -S (or --service) option of the
+<tt>knife cs server create</tt> command.
+
+=== knife cs template list
+Displays a list of all templates in the currently configured CloudStack account. Featured templates are displayed by default.
+Use the -F (or --filter) option to use a different filter. The allowable filter values are:
+
+* featured - templates that are featured and are public (this is the default)
+* self - templates that have been registered/created by the owner
+* self-executable - templates that have been registered/created by the owner that can be used to deploy a new VM
+* executable - all templates that can be used to deploy a new VM
+* community - templates that are public
+
+A template can be specified when creating a new server by passing the template name as an argument to the -T
+(or --template) option of the <tt>knife cs server create</tt> command.
+
+=== knife cs zone list
+Displays a list of all zones available in the currently configured CloudStack account. A zone can be specified
+when creating a new server by passing the zone name as an argument to the -Z (or --zone) option of the
+<tt>knife cs server create</tt> command.
+
+=== knife cs hosts
+Convenience command that displays the public ip address and fqdn for all servers. Matches /etc/hosts file format.
+
+=== knife cs stack create
+Creates a "stack" of servers based on a JSON definition file. Simple orchestration can be performed by
+specifying one or more actions to be executed after a server (or group of servers) is created.
+
+==== Example Stack Definition File:
+<tt>
+{
+ "name": "hadoop_cluster_a",
+ "description": "A small hadoop cluster with hbase",
+ "version": "1.0",
+ "environment": "production",
+ "servers": [
+ {
+ "name": "zookeeper-a, zookeeper-b, zookeeper-c",
+ "description": "Zookeeper nodes",
+ "template": "rhel-5.6-base",
+ "service": "small",
+ "port_rules": "2181",
+ "run_list": "role[cluster_a], role[zookeeper_server]",
+ "actions": [
+ { "knife_ssh": ["role:zookeeper_server", "sudo chef-client"] }
+ ]
+ },
+ {
+ "name": "hadoop-master",
+ "description": "Hadoop master node",
+ "template": "rhel-5.6-base",
+ "service": "large",
+ "networks": "app-net, storage-net",
+ "port_rules": "50070, 50030, 60010",
+ "run_list": "role[cluster_a], role[hadoop_master], role[hbase_master]"
+ },
+ {
+ "name": "hadoop-worker-a hadoop-worker-b hadoop-worker-c",
+ "description": "Hadoop worker nodes",
+ "template": "rhel-5.6-base",
+ "service": "medium",
+ "port_rules": "50075, 50060, 60030",
+ "run_list": "role[cluster_a], role[hadoop_worker], role[hbase_regionserver]",
+ "actions": [
+ { "knife_ssh": ["role:hadoop_master", "sudo chef-client"] },
+ { "http_request": "http://${hadoop-master}:50070/index.jsp" }
+ ]
+ }
+}
+</tt>
+
+==== Stack Attributes
+name:: Stack name. May be used in the future to e.g. look up a stack in a databag.
+version:: Stack version. This attribute is not currently used.
+environment:: Chef environment used for all servers in the stack.
+servers:: List of servers to create.
+
+==== Server Attributes
+name:: The name of the new server. Specify a list of names separated by commas and/or spaces to create multiple servers.
+description:: Server description.
+template:: Cloudstack template name.
+service:: Cloudstack service offering name.
+port_rules:: Port forwarding rules for virtual networks. These are ignored when using direct or other network types.
+run_list:: Chef run list.
+actions:: List of actions to perform after the server (or group of servers) is created.
+
+==== Actions
+knife-ssh:: Performs a knife-ssh command. Takes the search query and the command to execute as an array of arguments.
+http_request:: Performs an http request. Supports references to server names in the stack with ${server_name}.
+
+==== Upcoming Actions
+The following actions are under development:
+
+runlist_add:: Adds an entry from the run list.
+runlist_remove:: Removes an entry from the run list.
+
+=== knife cs stack delete
+Deletes all servers in the specified stack definition.
+
+=== knife cs server start
+Starts the specified virtual machine(s).
+
+=== knife cs server stop
+Stops the specified virtual machine(s). Force stop is optional.
+
+=== knife cs server reboot
+Reboots the specified virtual machines(s).
+
+== LICENSE:
+
+Author:: Ryan Holmes <rholmes@edmunds.com>
+Author:: KC Braunschweig <kbraunschweig@edmunds.com>
+Copyright:: Copyright (c) 2011 Edmunds, Inc.
+License:: Apache License, Version 2.0
+
+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.
+
3  Rakefile
@@ -0,0 +1,3 @@
+require "mg"
+
+MG.new("knife-cloudstack.gemspec")
17 knife-cloudstack.gemspec
@@ -0,0 +1,17 @@
+Gem::Specification.new do |s|
+ s.name = %q{knife-cloudstack}
+ s.version = "0.0.11"
+ s.date = %q{2011-11-03}
+ s.authors = ['Ryan Holmes', 'KC Braunschweig']
+ s.email = ['rholmes@edmunds.com', 'kbraunschweig@edmunds.com']
+ s.summary = %q{A knife plugin for the CloudStack API}
+ s.homepage = %q{http://www.edmunds.com/}
+ s.description = %q{A Knife plugin to create, list and manage CloudStack servers}
+
+ s.has_rdoc = true
+ s.extra_rdoc_files = ["README.rdoc", "CHANGES.rdoc", "LICENSE" ]
+
+ s.add_dependency "chef", ">= 0.10.0"
+ s.require_path = 'lib'
+ s.files = ["CHANGES.rdoc","README.rdoc", "LICENSE"] + Dir.glob("lib/**/*")
+end
83 lib/chef/knife/cs_hosts.rb
@@ -0,0 +1,83 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Author:: KC Braunschweig (<kbraunschweig@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsHosts < Chef::Knife
+
+ MEGABYTES = 1024 * 1024
+
+ deps do
+ require 'knife-cloudstack/connection'
+ end
+
+ banner "knife cs hosts"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ def run
+
+ connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+
+ host_list = [
+ ui.color('#Public IP', :bold),
+ ui.color('Host', :bold),
+ ui.color('FQDN', :bold)
+ ]
+
+ servers = connection.list_servers
+ pf_rules = connection.list_port_forwarding_rules
+ servers.each do |s|
+ host_list << (connection.get_server_public_ip(s, pf_rules) || '#')
+ host_list << (s['name'] || '')
+ host_list << (connection.get_server_fqdn(s) || '')
+ end
+ puts ui.list(host_list, :columns_across, 3)
+
+ end
+
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
84 lib/chef/knife/cs_network_list.rb
@@ -0,0 +1,84 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsNetworkList < Chef::Knife
+
+ deps do
+ require 'knife-cloudstack/connection'
+ end
+
+ banner "knife cs network list (options)"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ def run
+
+ connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+
+ network_list = [
+ ui.color('Name', :bold),
+ ui.color('Type', :bold),
+ ui.color('Default', :bold),
+ ui.color('Shared', :bold),
+ ui.color('Gateway', :bold),
+ ui.color('Netmask', :bold)
+ ]
+
+ networks = connection.list_networks
+ networks.each do |n|
+ network_list << n['name']
+ network_list << n['type']
+ network_list << n['isdefault'].to_s
+ network_list << n['isshared'].to_s
+ network_list << (n['gateway'] || '')
+ network_list << (n['netmask'] || '')
+ end
+ puts ui.list(network_list, :columns_across, 6)
+
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
315 lib/chef/knife/cs_server_create.rb
@@ -0,0 +1,315 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+require 'json'
+
+module KnifeCloudstack
+ class CsServerCreate < Chef::Knife
+
+ # Seconds to delay between detecting ssh and initiating the bootstrap
+ BOOTSTRAP_DELAY = 3
+
+ # Seconds to wait between ssh pings
+ SSH_POLL_INTERVAL = 2
+
+ deps do
+ require 'chef/knife/bootstrap'
+ Chef::Knife::Bootstrap.load_deps
+ require 'socket'
+ require 'net/ssh/multi'
+ require 'chef/json_compat'
+ require 'knife-cloudstack/connection'
+ end
+
+ banner "knife cs server create [SERVER_NAME] (options)"
+
+ option :cloudstack_service,
+ :short => "-S SERVICE",
+ :long => "--service SERVICE",
+ :description => "The CloudStack service offering name",
+ :proc => Proc.new { |o| Chef::Config[:knife][:cloudstack_service] = o },
+ :default => "M"
+
+ option :cloudstack_template,
+ :short => "-T TEMPLATE",
+ :long => "--template TEMPLATE",
+ :description => "The CloudStack template for the server",
+ :proc => Proc.new { |t| Chef::Config[:knife][:cloudstack_template] = t }
+
+ option :cloudstack_zone,
+ :short => "-Z ZONE",
+ :long => "--zone ZONE",
+ :description => "The CloudStack zone for the server",
+ :proc => Proc.new { |z| Chef::Config[:knife][:cloudstack_zone] = z }
+
+ option :cloudstack_networks,
+ :short => "-W NETWORKS",
+ :long => "--networks NETWORK",
+ :description => "Comma separated list of CloudStack network names",
+ :proc => lambda { |n| n.split(/[\s,]+/) },
+ :default => []
+
+ option :chef_node_name,
+ :short => "-N NAME",
+ :long => "--node-name NAME",
+ :description => "The Chef node name for your new node"
+
+ option :ssh_user,
+ :short => "-x USERNAME",
+ :long => "--ssh-user USERNAME",
+ :description => "The ssh username"
+
+ option :ssh_password,
+ :short => "-P PASSWORD",
+ :long => "--ssh-password PASSWORD",
+ :description => "The ssh password"
+
+ option :identity_file,
+ :short => "-i IDENTITY_FILE",
+ :long => "--identity-file IDENTITY_FILE",
+ :description => "The SSH identity file used for authentication"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack API endpoint URL",
+ :proc => Proc.new { |u| Chef::Config[:knife][:cloudstack_url] = u }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |k| Chef::Config[:knife][:cloudstack_api_key] = k }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |s| Chef::Config[:knife][:cloudstack_secret_key] = s }
+
+ option :prerelease,
+ :long => "--prerelease",
+ :description => "Install the pre-release chef gems"
+
+ option :bootstrap_version,
+ :long => "--bootstrap-version VERSION",
+ :description => "The version of Chef to install",
+ :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
+
+ option :distro,
+ :short => "-d DISTRO",
+ :long => "--distro DISTRO",
+ :description => "Bootstrap a distro using a template",
+ :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d },
+ :default => "ubuntu10.04-gems"
+
+ option :template_file,
+ :long => "--template-file TEMPLATE",
+ :description => "Full path to location of template to use",
+ :proc => Proc.new { |t| Chef::Config[:knife][:template_file] = t },
+ :default => false
+
+ option :run_list,
+ :short => "-r RUN_LIST",
+ :long => "--run-list RUN_LIST",
+ :description => "Comma separated list of roles/recipes to apply",
+ :proc => lambda { |o| o.split(/[\s,]+/) },
+ :default => []
+
+ option :no_host_key_verify,
+ :long => "--no-host-key-verify",
+ :description => "Disable host key verification",
+ :boolean => true,
+ :default => false
+
+ option :no_bootstrap,
+ :long => "--no-bootstrap",
+ :description => "Disable Chef bootstrap",
+ :boolean => true,
+ :default => false
+
+ option :port_rules,
+ :short => "-p PORT_RULES",
+ :long => "--port-rules PORT_RULES",
+ :description => "Comma separated list of port forwarding rules, e.g. '25,53:4053,80:8080:TCP'",
+ :proc => lambda { |o| o.split(/[\s,]+/) },
+ :default => []
+
+
+ def run
+
+ # validate hostname and options
+ hostname = @name_args.first
+ unless /^[a-zA-Z0-9][a-zA-Z0-9-]*$/.match hostname then
+ ui.error "Invalid hostname. Please specify a short hostname, not an fqdn (e.g. 'myhost' instead of 'myhost.domain.com')."
+ exit 1
+ end
+ validate_options
+
+ $stdout.sync = true
+
+ connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+
+ print "#{ui.color("Waiting for server", :magenta)}"
+ server = connection.create_server(
+ hostname,
+ locate_config_value(:cloudstack_service),
+ locate_config_value(:cloudstack_template),
+ locate_config_value(:cloudstack_zone),
+ locate_config_value(:cloudstack_networks)
+ )
+
+ public_ip = find_or_create_public_ip(server, connection)
+
+ puts "\n\n"
+ puts "#{ui.color("Name", :cyan)}: #{server['name']}"
+ puts "#{ui.color("Public IP", :cyan)}: #{public_ip}"
+
+ return if config[:no_bootstrap]
+
+ print "\n#{ui.color("Waiting for sshd", :magenta)}"
+
+ print(".") until is_ssh_open?(public_ip) {
+ sleep BOOTSTRAP_DELAY
+ puts "\n"
+ }
+
+ bootstrap_for_node(public_ip).run
+
+ puts "\n"
+ puts "#{ui.color("Name", :cyan)}: #{server['name']}"
+ puts "#{ui.color("Public IP", :cyan)}: #{public_ip}"
+ puts "#{ui.color("Environment", :cyan)}: #{config[:environment] || '_default'}"
+ puts "#{ui.color("Run List", :cyan)}: #{config[:run_list].join(', ')}"
+
+ end
+
+ def validate_options
+
+ unless locate_config_value :cloudstack_template
+ ui.error "Cloudstack template not specified"
+ exit 1
+ end
+
+ unless locate_config_value :cloudstack_service
+ ui.error "Cloudstack service offering not specified"
+ exit 1
+ end
+
+ identity_file = locate_config_value :identity_file
+ ssh_user = locate_config_value :ssh_user
+ ssh_password = locate_config_value :ssh_password
+ unless identity_file || (ssh_user && ssh_password)
+ ui.error("You must specify either an ssh identity file or an ssh user and password")
+ exit 1
+ end
+ end
+
+
+ def find_or_create_public_ip(server, connection)
+ nic = connection.get_server_default_nic(server) || {}
+ if nic['type'] == 'Virtual' then
+ # create ip address, ssh forwarding rule and optional forwarding rules
+ ip_address = connection.associate_ip_address(server['zoneid'])
+ ssh_rule = connection.create_port_forwarding_rule(ip_address['id'], "22", "TCP", "22", server['id'])
+ create_port_forwarding_rules(ip_address['id'], server['id'], connection)
+ ssh_rule['ipaddress']
+ else
+ # otherwise return the nic ip address
+ nic['ipaddress']
+ end
+ end
+
+ def create_port_forwarding_rules(ip_address_id, server_id, connection)
+ rules = locate_config_value(:port_rules)
+ return unless rules
+
+ rules.each do |rule|
+ args = rule.split(':')
+ public_port = args[0]
+ private_port = args[1] || args[0]
+ protocol = args[2] || "TCP"
+ connection.create_port_forwarding_rule(ip_address_id, private_port, protocol, public_port, server_id)
+ end
+
+ end
+
+ #noinspection RubyArgCount,RubyResolve
+ def is_ssh_open?(ip)
+ s = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
+ sa = Socket.sockaddr_in(22, ip)
+
+ begin
+ s.connect_nonblock(sa)
+ rescue Errno::EINPROGRESS
+ resp = IO.select(nil, [s], nil, 1)
+ if resp.nil?
+ sleep SSH_POLL_INTERVAL
+ return false
+ end
+
+ begin
+ s.connect_nonblock(sa)
+ rescue Errno::EISCONN
+ Chef::Log.debug("sshd accepting connections on #{ip}")
+ yield
+ return true
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
+ sleep SSH_POLL_INTERVAL
+ return false
+ end
+ ensure
+ s && s.close
+ end
+ end
+
+
+ def bootstrap_for_node(host)
+ bootstrap = Chef::Knife::Bootstrap.new
+ bootstrap.name_args = [host]
+ bootstrap.config[:run_list] = config[:run_list]
+ bootstrap.config[:ssh_user] = config[:ssh_user]
+ bootstrap.config[:ssh_password] = config[:ssh_password]
+ bootstrap.config[:identity_file] = config[:identity_file]
+ bootstrap.config[:chef_node_name] = config[:chef_node_name] if config[:chef_node_name]
+ bootstrap.config[:prerelease] = config[:prerelease]
+ bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
+ bootstrap.config[:distro] = locate_config_value(:distro)
+ bootstrap.config[:use_sudo] = true
+ bootstrap.config[:template_file] = locate_config_value(:template_file)
+ bootstrap.config[:environment] = config[:environment]
+ # may be needed for vpc_mode
+ bootstrap.config[:no_host_key_verify] = config[:no_host_key_verify]
+ bootstrap
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end # class
+end
+
+
+
157 lib/chef/knife/cs_server_delete.rb
@@ -0,0 +1,157 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsServerDelete < Chef::Knife
+
+ deps do
+ require 'knife-cloudstack/connection'
+ require 'chef/api_client'
+ end
+
+ banner "knife cs server delete SERVER_NAME [SERVER_NAME ...] (options)"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ def run
+
+ @name_args.each do |hostname|
+ server = connection.get_server(hostname)
+
+ if !server then
+ ui.error("Server '#{hostname}' not found")
+ next
+ end
+
+ if server['state'] == 'Destroyed' then
+ ui.warn("Server '#{hostname}' already destroyed")
+ next
+ end
+
+ puts "\n"
+ msg("Name", server['name'])
+ msg("Public IP", connection.get_server_public_ip(server) || '?')
+ msg("Service", server['serviceofferingname'])
+ msg("Template", server['templatename'])
+ msg("Domain", server['domain'])
+ msg("Zone", server['zonename'])
+ msg("State", server['state'])
+
+ puts "\n"
+ ui.confirm("Do you really want to delete this server")
+
+ print "#{ui.color("Waiting for deletion", :magenta)}"
+ disassociate_virtual_ip_address server
+ connection.delete_server hostname
+ puts "\n"
+ ui.msg("Deleted server #{hostname}")
+
+ # delete chef client and node
+ node_name = connection.get_server_fqdn server
+ ui.confirm("Do you want to delete the chef node and client '#{node_name}")
+ delete_node node_name
+ delete_client node_name
+ end
+
+ end
+
+ def disassociate_virtual_ip_address(server)
+ nic = server['nic'].first || {}
+ return unless nic['type'] == 'Virtual'
+
+ # get the ssh rule for this server
+ ssh_rule = connection.get_ssh_port_forwarding_rule(server)
+ return unless ssh_rule
+
+ # get all rules for the same ip address
+ rules = connection.list_port_forwarding_rules(ssh_rule['ipaddressid'])
+ return unless rules
+
+ # ensure ip address has rules only for this server
+ rules.each { |r|
+ return if r['virtualmachineid'] != server['id']
+ }
+
+ # dissassociate the ip address if all tests passed
+ connection.disassociate_ip_address(ssh_rule['ipaddressid'])
+ end
+
+ def delete_client(name)
+ begin
+ client = Chef::ApiClient.load(name)
+ rescue Net::HTTPServerException
+ return
+ end
+
+ client.destroy
+ ui.msg "Deleted client #{name}"
+ end
+
+ def delete_node(name)
+ begin
+ node = Chef::Node.load(name)
+ rescue Net::HTTPServerException
+ return
+ end
+
+ node.destroy
+ ui.msg "Deleted node #{name}"
+ end
+
+ def connection
+ unless @connection
+ @connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+ end
+ @connection
+ end
+
+ def msg(label, value)
+ if value && !value.empty?
+ puts "#{ui.color(label, :cyan)}: #{value}"
+ end
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
92 lib/chef/knife/cs_server_list.rb
@@ -0,0 +1,92 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsServerList < Chef::Knife
+
+ deps do
+ require 'knife-cloudstack/connection'
+ end
+
+ banner "knife cs server list (options)"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ def run
+
+ $stdout.sync = true
+
+ connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+
+ server_list = [
+ ui.color('Name', :bold),
+ ui.color('Public IP', :bold),
+ ui.color('Service', :bold),
+ ui.color('Template', :bold),
+ ui.color('State', :bold)
+ ]
+
+ servers = connection.list_servers
+ rules = connection.list_port_forwarding_rules
+
+ servers.each do |server|
+
+ name = server['name']
+ display_name = server['displayname']
+ if display_name && !display_name.empty? && display_name != name
+ name << " (#{display_name})"
+ end
+ server_list << server['name']
+ server_list << (connection.get_server_public_ip(server, rules) || '')
+ server_list << server['serviceofferingname']
+ server_list << server['templatename']
+ server_list << server['state']
+ end
+ puts ui.list(server_list, :columns_across, 5)
+
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
103 lib/chef/knife/cs_server_reboot.rb
@@ -0,0 +1,103 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Author:: KC Braunschweig (<kbraunschweig@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsServerReboot < Chef::Knife
+
+ deps do
+ require 'knife-cloudstack/connection'
+ require 'chef/api_client'
+ end
+
+ banner "knife cs server reboot SERVER_NAME [SERVER_NAME ...] (options)"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ def run
+
+ @name_args.each do |hostname|
+ server = connection.get_server(hostname)
+
+ if !server then
+ ui.error("Server '#{hostname}' not found")
+ next
+ end
+
+ puts "\n"
+ msg("Name", server['name'])
+ msg("Public IP", connection.get_server_public_ip(server) || '?')
+ msg("Service", server['serviceofferingname'])
+ msg("Template", server['templatename'])
+ msg("Domain", server['domain'])
+ msg("Zone", server['zonename'])
+ msg("State", server['state'])
+
+ puts "\n"
+ ui.confirm("Do you really want to reboot this server")
+ print "#{ui.color("Rebooting", :magenta)}"
+
+ connection.reboot_server(hostname)
+ puts "\n"
+ ui.msg("Rebooted server #{hostname}")
+ end
+
+ end
+
+ def connection
+ unless @connection
+ @connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+ end
+ @connection
+ end
+
+ def msg(label, value)
+ if value && !value.empty?
+ puts "#{ui.color(label, :cyan)}: #{value}"
+ end
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
103 lib/chef/knife/cs_server_start.rb
@@ -0,0 +1,103 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Author:: KC Braunschweig (<kbraunschweig@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsServerStart < Chef::Knife
+
+ deps do
+ require 'knife-cloudstack/connection'
+ require 'chef/api_client'
+ end
+
+ banner "knife cs server start SERVER_NAME [SERVER_NAME ...] (options)"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ def run
+
+ @name_args.each do |hostname|
+ server = connection.get_server(hostname)
+
+ if !server then
+ ui.error("Server '#{hostname}' not found")
+ next
+ end
+
+ puts "\n"
+ msg("Name", server['name'])
+ msg("Public IP", connection.get_server_public_ip(server) || '?')
+ msg("Service", server['serviceofferingname'])
+ msg("Template", server['templatename'])
+ msg("Domain", server['domain'])
+ msg("Zone", server['zonename'])
+ msg("State", server['state'])
+
+ puts "\n"
+ ui.confirm("Do you really want to start this server")
+
+ print "#{ui.color("Waiting for startup", :magenta)}"
+ connection.start_server(hostname)
+ puts "\n"
+ ui.msg("Started server #{hostname}")
+ end
+
+ end
+
+ def connection
+ unless @connection
+ @connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+ end
+ @connection
+ end
+
+ def msg(label, value)
+ if value && !value.empty?
+ puts "#{ui.color(label, :cyan)}: #{value}"
+ end
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
114 lib/chef/knife/cs_server_stop.rb
@@ -0,0 +1,114 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Author:: KC Braunschweig (<kbraunschweig@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsServerStop < Chef::Knife
+
+ deps do
+ require 'knife-cloudstack/connection'
+ require 'chef/api_client'
+ end
+
+ banner "knife cs server stop SERVER_NAME [SERVER_NAME ...] (options)"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ option :cloudstack_force_stop,
+ :long => "--force",
+ :description => "Force stop the VM. The caller knows the VM is stopped.",
+ :boolean => true
+
+ def run
+
+ @name_args.each do |hostname|
+ server = connection.get_server(hostname)
+
+ if !server then
+ ui.error("Server '#{hostname}' not found")
+ next
+ end
+
+ puts "\n"
+ msg("Name", server['name'])
+ msg("Public IP", connection.get_server_public_ip(server) || '?')
+ msg("Service", server['serviceofferingname'])
+ msg("Template", server['templatename'])
+ msg("Domain", server['domain'])
+ msg("Zone", server['zonename'])
+ msg("State", server['state'])
+
+ puts "\n"
+ if config[:cloudstack_force_stop]
+ ui.confirm("Do you really want to force stop this server")
+ print "#{ui.color("Forcefully stopping", :magenta)}"
+ connection.stop_server(hostname,config[:cloudstack_force_stop])
+ else
+ ui.confirm("Do you really want to stop this server")
+ print "#{ui.color("Stopping", :magenta)}"
+ connection.stop_server(hostname)
+ end
+
+ puts "\n"
+ ui.msg("Stopped server #{hostname}")
+ end
+
+ end
+
+ def connection
+ unless @connection
+ @connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+ end
+ @connection
+ end
+
+ def msg(label, value)
+ if value && !value.empty?
+ puts "#{ui.color(label, :cyan)}: #{value}"
+ end
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
93 lib/chef/knife/cs_service_list.rb
@@ -0,0 +1,93 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsServiceList < Chef::Knife
+
+ MEGABYTES = 1024 * 1024
+
+ deps do
+ require 'knife-cloudstack/connection'
+ end
+
+ banner "knife cs service list (options)"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ def run
+
+ connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+
+ service_list = [
+ ui.color('Name', :bold),
+ ui.color('Memory', :bold),
+ ui.color('CPUs', :bold),
+ ui.color('CPU Speed', :bold),
+ ui.color('Created', :bold)
+ ]
+
+ services = connection.list_service_offerings
+ services.each do |s|
+ service_list << s['name']
+ service_list << (human_memory(s['memory']) || 'Unknown')
+ service_list << s['cpunumber'].to_s
+ service_list << s['cpuspeed'].to_s + ' Mhz'
+ service_list << s['created']
+ end
+ puts ui.list(service_list, :columns_across, 5)
+
+ end
+
+ def human_memory n
+ count = 0
+ while n >= 1024 and count < 2
+ n /= 1024.0
+ count += 1
+ end
+ format("%.2f", n) + %w(MB GB TB)[count]
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
325 lib/chef/knife/cs_stack_create.rb
@@ -0,0 +1,325 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsStackCreate < Chef::Knife
+
+ attr_accessor :current_stack
+
+ deps do
+ require 'chef/json_compat'
+ require 'chef/mash'
+ require 'chef/search/query'
+ require 'chef/knife/node_run_list_remove'
+ require 'net/ssh'
+ require 'net/ssh/multi'
+ require 'knife-cloudstack/connection'
+ Chef::Knife::Ssh.load_deps
+ Chef::Knife::NodeRunListRemove.load_deps
+ KnifeCloudstack::CsServerCreate.load_deps
+ end
+
+ banner "knife cs stack create JSON_FILE (options)"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ option :ssh_user,
+ :short => "-x USERNAME",
+ :long => "--ssh-user USERNAME",
+ :description => "The ssh username"
+
+ option :ssh_password,
+ :short => "-P PASSWORD",
+ :long => "--ssh-password PASSWORD",
+ :description => "The ssh password"
+
+ option :identity_file,
+ :short => "-i IDENTITY_FILE",
+ :long => "--identity-file IDENTITY_FILE",
+ :description => "The SSH identity file used for authentication"
+
+ def run
+ file_path = File.expand_path(@name_args.first)
+ unless File.exist?(file_path) then
+ ui.error "Stack file '#{file_path}' not found. Please check the path."
+ exit 1
+ end
+
+ data = File.read file_path
+ stack = Chef::JSONCompat.from_json data
+ create_stack stack
+
+ #puts "Stack: #{stack.inspect}"
+ end
+
+ def connection
+ if (!@connection) then
+ url = locate_config_value(:cloudstack_url)
+ api_key = locate_config_value(:cloudstack_api_key)
+ secret_key = locate_config_value(:cloudstack_secret_key)
+ @connection = CloudstackClient::Connection.new(url, api_key, secret_key)
+ end
+ @connection
+ end
+
+ def create_stack(stack)
+ @current_stack = Mash.new(stack)
+ current_stack[:servers].each do |server|
+ if server[:name]
+
+ # create server(s)
+ names = server[:name].split(/[\s,]+/)
+ names.each do |n|
+ s = Mash.new(server)
+ s[:name] = n
+ create_server(s)
+ end
+
+ end
+
+ # execute actions
+ run_actions server[:actions]
+ end
+
+ print_local_hosts
+ end
+
+ def create_server(server)
+
+ cmd = KnifeCloudstack::CsServerCreate.new([server[:name]])
+
+ # configure and run command
+ # TODO: validate parameters
+ cmd.config[:ssh_user] = config[:ssh_user]
+ cmd.config[:ssh_password] = config[:ssh_password]
+ cmd.config[:ssh_port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
+ cmd.config[:identity_file] = config[:identity_file]
+ cmd.config[:cloudstack_template] = server[:template] if server[:template]
+ cmd.config[:cloudstack_service] = server[:service] if server[:service]
+ cmd.config[:cloudstack_zone] = server[:service] if server[:zone]
+ cmd.config[:cloudstack_networks] = server[:networks].split(/[\s,]+/) if server[:networks]
+ cmd.config[:run_list] = server[:run_list].split(/[\s,]+/) if server[:run_list]
+ cmd.config[:port_rules] = server[:port_rules].split(/[\s,]+/) if server[:port_rules]
+ if current_stack[:environment]
+ cmd.config[:environment] = current_stack[:environment]
+ Chef::Config[:environment] = current_stack[:environment]
+ end
+
+ cmd.run_with_pretty_exceptions
+
+ end
+
+ def run_actions(actions)
+ puts "\n"
+ ui.msg("Processing actions...")
+ sleep 1 # pause for e.g. chef solr indexing
+ actions ||= []
+ actions.each do |cmd|
+ cmd ||= {}
+ cmd.each do |name, args|
+ case name
+ when 'knife_ssh'
+ knife_ssh_action(*args)
+ when 'http_request'
+ http_request(args)
+ when 'run_list_remove'
+ run_list_remove(*args)
+ when 'sleep'
+ dur = args || 5
+ sleep dur
+ end
+ end
+ end
+
+ end
+
+ def search_nodes(query, attribute=nil)
+ if get_environment
+ query = "(#{query})" + " AND chef_environment:#{get_environment}"
+ end
+
+ Chef::Log.debug("Searching for nodes: #{query}")
+
+ q = Chef::Search::Query.new
+ nodes = Array(q.search(:node, query))
+
+ # the array of nodes is the first item in the array returned by the search
+ if nodes.length > 1
+ nodes = nodes.first || []
+ end
+
+ # return attribute values instead of nodes
+ if attribute
+ nodes.map do |node|
+ node[attribute.to_s]
+ end
+ else
+ nodes
+ end
+ end
+
+ def knife_ssh(host_list, command)
+
+ ssh = Chef::Knife::Ssh.new
+ ssh.name_args = [host_list, command]
+ ssh.config[:ssh_user] = config[:ssh_user]
+ ssh.config[:ssh_password] = config[:ssh_password]
+ ssh.config[:ssh_port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
+ ssh.config[:identity_file] = config[:identity_file]
+ ssh.config[:manual] = true
+ ssh.config[:no_host_key_verify] = config[:no_host_key_verify]
+ ssh
+ end
+
+ def knife_ssh_with_password_auth(host_list, command)
+ ssh = knife_ssh(host_list, command)
+ ssh.config[:identity_file] = nil
+ ssh.config[:ssh_password] = ssh.get_password
+ ssh
+ end
+
+ def knife_ssh_action(query, command)
+
+ public_ips = find_public_ips(query)
+ return if public_ips.nil? || public_ips.empty?
+ host_list = public_ips.join(' ')
+
+ ssh = knife_ssh(host_list, command)
+ begin
+ ssh.run
+ rescue Net::SSH::AuthenticationFailed
+ unless config[:ssh_password]
+ puts "Failed to authenticate #{config[:ssh_user]} - trying password auth"
+ ssh = knife_ssh_with_password_auth(host_list, command)
+ ssh.run
+ end
+ end
+
+ end
+
+ def http_request(url)
+ match_data = /\$\{([a-zA-Z0-9-]+)\}/.match url
+ if match_data
+ server_name = match_data[1]
+ ip = public_ip_for_host(server_name)
+ url = url.sub(/\$\{#{server_name}\}/, ip)
+ end
+
+
+ puts "HTTP Request: #{url}"
+ puts `curl -s -m 5 #{url}`
+ end
+
+ def run_list_remove(query, entry)
+ nodes = search_nodes(query)
+ return unless nodes
+
+ nodes.each do |n|
+ cmd = Chef::Knife::NodeRunListRemove.new([n.name, entry])
+ cmd.run_with_pretty_exceptions
+ end
+ end
+
+ def find_public_ips(query)
+ hostnames = search_nodes(query, 'hostname')
+ puts "Found hostnames: #{hostnames.inspect}"
+ ips = hostnames.map { |h|
+ public_ip_for_host h
+ }
+ ips.compact.uniq
+ end
+
+ def public_ip_for_host(name)
+ return nil unless name
+ @public_ip_cache ||= {}
+
+ if !@public_ip_cache[name] then
+ server = connection.get_server(name)
+ return nil unless server
+
+ ip = connection.get_server_public_ip(server)
+ @public_ip_cache[name] = ip if ip
+ end
+
+ @public_ip_cache[name]
+ end
+
+ def get_environment
+ current_stack[:environment]
+ end
+
+ def destroy_all(domain, excludes=[])
+ servers = connection.list_servers || []
+ servers.each do |s|
+ excluded = false
+ excludes.each { |val|
+ if s['name'] =~ /#{val}/ then
+ excluded = true
+ next
+ end
+ }
+ next if excluded
+ nodename = "#{s['name']}.#{domain}"
+ system "knife cs server delete #{s['name']} -y"
+ system "knife client delete #{nodename} -y"
+ system "knife node delete #{nodename} -y"
+ end
+ end
+
+ def print_local_hosts
+ hosts = []
+ current_stack[:servers].each do |server|
+ next unless server[:local_hosts]
+ name = server[:name].split(' ').first
+ ip = public_ip_for_host(name)
+ server[:local_hosts].each { |host|
+ hostname = host.sub(/\$\{environment\}/, get_environment)
+ hosts << "#{ip} #{hostname}"
+ }
+ end
+ unless hosts.empty?
+ puts "\nAdd this to your /etc/hosts file:"
+ puts hosts.join("\n")
+ end
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
88 lib/chef/knife/cs_stack_delete.rb
@@ -0,0 +1,88 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsStackDelete < Chef::Knife
+
+ deps do
+ require 'chef/json_compat'
+ require 'chef/mash'
+ require 'knife-cloudstack/connection'
+ KnifeCloudstack::CsServerDelete.load_deps
+ end
+
+ banner "knife cs stack delete JSON_FILE (options)"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ def run
+ file_path = File.expand_path(@name_args.first)
+ unless File.exist?(file_path) then
+ ui.error "Stack file '#{file_path}' not found. Please check the path."
+ exit 1
+ end
+
+ data = File.read file_path
+ stack = Chef::JSONCompat.from_json data
+ delete_stack stack
+
+ end
+
+ def delete_stack(stack)
+ current_stack = Mash.new(stack)
+ current_stack[:servers].each do |server|
+ if server[:name]
+
+ # delete server(s)
+ names = server[:name].split(/[\s,]+/)
+ names.each do |name|
+ delete_server(name)
+ end
+
+ end
+
+ end
+ end
+
+ def delete_server(server_name)
+ cmd = KnifeCloudstack::CsServerDelete.new([server_name])
+ cmd.config[:yes] = true
+ cmd.run_with_pretty_exceptions
+ end
+
+ end
+
+end
100 lib/chef/knife/cs_template_list.rb
@@ -0,0 +1,100 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsTemplateList < Chef::Knife
+
+ MEGABYTES = 1024 * 1024
+
+ deps do
+ require 'knife-cloudstack/connection'
+ end
+
+ banner "knife cs template list (options)"
+
+ option :filter,
+ :short => "-L FILTER",
+ :long => "--filter FILTER",
+ :description => "The template search filter. Default is 'featured'",
+ :default => "featured"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ def run
+
+ connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+
+ template_list = [
+ ui.color('Name', :bold),
+ ui.color('Size', :bold),
+ ui.color('Zone', :bold),
+ ui.color('Public', :bold),
+ ui.color('Created', :bold),
+ ]
+
+ filter = config['filter']
+ templates = connection.list_templates(filter)
+ templates.each do |t|
+ template_list << t['name']
+ template_list << (human_file_size(t['size']) || 'Unknown')
+ template_list << t['zonename']
+ template_list << t['ispublic'].to_s
+ template_list << t['created']
+ end
+ puts ui.list(template_list, :columns_across, 5)
+
+ end
+
+ def human_file_size n
+ count = 0
+ while n >= 1024 and count < 4
+ n /= 1024.0
+ count += 1
+ end
+ format("%.2f", n) + %w(B KB MB GB TB)[count]
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
78 lib/chef/knife/cs_zone_list.rb
@@ -0,0 +1,78 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'chef/knife'
+
+module KnifeCloudstack
+ class CsZoneList < Chef::Knife
+
+ deps do
+ require 'knife-cloudstack/connection'
+ end
+
+ banner "knife cs zone list (options)"
+
+ option :cloudstack_url,
+ :short => "-U URL",
+ :long => "--cloudstack-url URL",
+ :description => "The CloudStack endpoint URL",
+ :proc => Proc.new { |url| Chef::Config[:knife][:cloudstack_url] = url }
+
+ option :cloudstack_api_key,
+ :short => "-A KEY",
+ :long => "--cloudstack-api-key KEY",
+ :description => "Your CloudStack API key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_api_key] = key }
+
+ option :cloudstack_secret_key,
+ :short => "-K SECRET",
+ :long => "--cloudstack-secret-key SECRET",
+ :description => "Your CloudStack secret key",
+ :proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
+
+ def run
+
+ connection = CloudstackClient::Connection.new(
+ locate_config_value(:cloudstack_url),
+ locate_config_value(:cloudstack_api_key),
+ locate_config_value(:cloudstack_secret_key)
+ )
+
+ zone_list = [
+ ui.color('Name', :bold),
+ ui.color('Network Type', :bold),
+ ui.color('Security Groups', :bold)
+ ]
+
+ zones = connection.list_zones
+ zones.each do |z|
+ zone_list << z['name']
+ zone_list << z['networktype']
+ zone_list << z['securitygroupsenabled'].to_s
+ end
+ puts ui.list(zone_list, :columns_across, 3)
+
+ end
+
+ def locate_config_value(key)
+ key = key.to_sym
+ Chef::Config[:knife][key] || config[key]
+ end
+
+ end
+end
604 lib/knife-cloudstack/connection.rb
@@ -0,0 +1,604 @@
+#
+# Author:: Ryan Holmes (<rholmes@edmunds.com>)
+# Author:: KC Braunschweig (<kbraunschweig@edmunds.com>)
+# Copyright:: Copyright (c) 2011 Edmunds, Inc.
+# License:: Apache License, Version 2.0
+#
+# 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.
+#
+
+require 'rubygems'
+require 'base64'
+require 'openssl'
+require 'uri'
+require 'cgi'
+require 'net/http'
+require 'json'
+
+module CloudstackClient
+ class Connection
+
+ ASYNC_POLL_INTERVAL = 2.0
+ ASYNC_TIMEOUT = 300
+
+ def initialize(api_url, api_key, secret_key)
+ @api_url = api_url
+ @api_key = api_key
+ @secret_key = secret_key
+ end
+
+ ##
+ # Finds the server with the specified name.
+
+ def get_server(name)
+ params = {
+ 'command' => 'listVirtualMachines',
+ 'name' => name
+ }
+ json = send_request(params)
+ machines = json['virtualmachine']
+
+ if !machines || machines.empty? then
+ return nil
+ end
+
+ machines.first
+ end
+
+ ##
+ # Finds the public ip for a server
+
+ def get_server_public_ip(server, cached_rules=nil)
+ return nil unless server
+
+ # find the public ip
+ nic = get_server_default_nic(server) || {}
+ if nic['type'] == 'Virtual' then
+ ssh_rule = get_ssh_port_forwarding_rule(server, cached_rules)
+ ssh_rule ? ssh_rule['ipaddress'] : nil
+ else
+ nic['ipaddress']
+ end
+ end
+
+ ##
+ # Returns the fully qualified domain name for a server.
+
+ def get_server_fqdn(server)
+ return nil unless server
+
+ nic = get_server_default_nic(server) || {}
+ networks = list_networks || {}
+
+ id = nic['networkid']
+ network = networks.select { |net|
+ net['id'] == id
+ }.first
+ return nil unless network
+
+ "#{server['name']}.#{network['networkdomain']}"
+ end
+
+ def get_server_default_nic(server)
+ server['nic'].each do |nic|
+ return nic if nic['isdefault']
+ end
+ end
+
+ ##
+ # Lists all the servers in your account.
+
+ def list_servers
+ params = {
+ 'command' => 'listVirtualMachines'
+ }
+ json = send_request(params)
+ json['virtualmachine'] || []
+ end
+
+ ##
+ # Deploys a new server using the specified parameters.
+
+ def create_server(host_name, service_name, template_name, zone_name=nil, network_names=[])
+
+ if host_name then
+ if get_server(host_name) then
+ puts "Error: Server '#{host_name}' already exists."
+ exit 1
+ end
+ end
+
+ service = get_service_offering(service_name)
+ if !service then
+ puts "Error: Service offering '#{service_name}' is invalid"
+ exit 1
+ end
+
+ template = get_template(template_name)
+ if !template then