From 44d2ba5f326e74da2d108ea560a06f04a0729129 Mon Sep 17 00:00:00 2001 From: Chris Howe Date: Mon, 10 Mar 2014 13:14:44 -0500 Subject: [PATCH] feature: adding openstack and static cloud support This change sets up basic ironfan features for use with an openstack cloud, as well as adding a fake cloud provider called "static" that lets you specify (normally) cloud-discoverable parameters directly in the cluster definition. Squashed commit of the following: commit a80592a2adf35ca6db3821c2b7897b9e6dad603b Merge: b4149c9 1572b76 Author: Chris Howe Date: Mon Mar 10 13:12:17 2014 -0500 Merge branch 'master' of github.com:howech/ironfan into 20140310-feature-openstack-support Conflicts: Gemfile Gemfile.lock ironfan.gemspec lib/ironfan/dsl/component.rb commit 1572b764e64f0f90a83ef380dfdd15e89d08f04e Merge: 1925e5e 5edcf9a Author: Chris Howe Date: Tue Feb 25 10:58:47 2014 -0600 Merge pull request #4 from erikmack/master display floating IP's, also bump excon commit 1925e5e84943cb523877f5e097dbe79db88b4277 Merge: 5a18c58 be86033 Author: Chris Howe Date: Wed Feb 19 15:06:55 2014 -0600 Merge pull request #3 from howech/static-cloud Static cloud commit be860331599c6071c1f124964be44bb667fddaea Author: Chris Howe Date: Wed Feb 19 14:58:54 2014 -0600 minor changes to help the world work better commit 5edcf9abb98d1fcf671f1129bbd5f5e41f3b691a Author: Erik Mackdanz Date: Wed Feb 19 14:49:15 2014 -0600 Bump excon dep commit fe78a40f13bc789cd99f19199d7c14ca28f6aa18 Author: Erik Mackdanz Date: Wed Feb 19 12:56:44 2014 -0600 Display floating IP address in public IP fields commit 5a18c585d6a56b66faea1b6bf0cd39099a4f0b7e Merge: 79f5289 bb88ae4 Author: Chris Howe Date: Wed Feb 19 12:28:19 2014 -0600 Merge pull request #2 from erikmack/master Factor out cloud-specific security group code commit 06c92dd83845b9ef7f38669f26c86262ed01c78a Author: Chris Howe Date: Fri Feb 7 11:56:40 2014 -0600 Added a non-cloud cloud provider called 'static' commit 79f52890cc939e7c83ee8dc4e7d2851302a9bbde Author: Chris Howe Date: Wed Jan 29 16:05:59 2014 -0600 Added elastic ip support for openstack. Shored up public ip address code. commit bb88ae4bae5baae21678d6cfc4acded28cb37480 Author: Erik Mackdanz Date: Tue Jan 28 16:33:16 2014 -0600 Factor out cloud-specific security group code commit b5591dcaa5f6a1797386a9064d8dc2d81f6ceb93 Author: Chris Howe Date: Mon Jan 27 13:00:54 2014 -0600 machine state tweaks for openstack commit 353862f105cb93966a35feb54ed6edbd8b762a28 Author: Chris Howe Date: Fri Jan 17 15:39:20 2014 -0600 Fixed an unintended edit :Fixed an unintende commit dbd9f67af34d5015560f472875e6064f2dd3851a Author: Chris Howe Date: Fri Jan 17 14:37:48 2014 -0600 changed wait for ssh behavior to resect ssh rules commit 0ed441a6846109775c868da05b23b8f158703260 Author: Chris Howe Date: Fri Jan 17 11:27:01 2014 -0600 changed cluster and facet groups to use server.security_group instead of cloud.security group commit c72a46638eec589f1deecebf956f6ff2e39ea4dc Author: Chris Howe Date: Thu Jan 16 16:35:13 2014 -0600 Added a security_groups container to dsl/compute, and changed providers to respect both cloud defined and compute defined security groups. commit 610cf62d5310bdc583b543bd3fdc120a9ac8b9a5 Merge: 6f99623 1b82f8f Author: Chris Howe Date: Tue Jan 14 11:22:42 2014 -0800 Merge pull request #1 from erikmack/master Make the DNS search domain configurable at the cloud layer. commit 1b82f8f96f3c539a6d96afc05833c3677e51256e Author: Erik Mackdanz Date: Tue Jan 14 12:46:56 2014 -0600 Make the DNS search domain configurable at the cloud layer. The NFS server will perform a reverse DNS query on any clients that try to connect. Only clients whose reverse DNS response matches this domain are allowed to connect. commit 6f99623aaeddc12cb9aad7ea320da4186c7221a9 Author: Chris Howe Date: Mon Jan 13 10:15:17 2014 -0600 fixed security groups for openstack commit cd8f930ae70a8b9cbb5c6d1e8af09e72bd313c75 Author: Chris Howe Date: Sun Jan 12 10:01:34 2014 -0600 Added some better statuses. Changed the default to actually wait for ssh ports. commit bee2a0d2ddc495990bf8fafc9250b39d3a4f50c0 Author: Chris Howe Date: Fri Jan 10 04:51:34 2014 +0000 fixed a keypair bug commit 8788ccfc990227c144c04665e7c2e48cb90e2879 Author: Chris Howe Date: Thu Jan 9 19:34:01 2014 +0000 better support for public/private ip address commit e92811a4d3259e048196f8dc10eed076c7233ee5 Author: Chris Howe Date: Thu Jan 9 19:32:57 2014 +0000 better support for public/private ip address commit f756cce6b87baa753efd7fd6f81dc970bca78ab7 Author: Chris Howe Date: Wed Jan 8 15:45:44 2014 +0000 Added basic support for an openstack cloud provider. Change-Id: If34e82013bdb321d8114d6a507ef84df3567b9ed --- Gemfile | 1 + Gemfile.lock | 198 ++++++++++ ironfan.gemspec | 1 + .../knife/bootstrap/ubuntu12.04-ironfan.erb | 7 + lib/chef/knife/cluster_launch.rb | 39 +- lib/chef/knife/ironfan_knife_common.rb | 35 ++ lib/ironfan/dsl/cloud.rb | 2 + lib/ironfan/dsl/component.rb | 7 +- lib/ironfan/dsl/compute.rb | 1 + lib/ironfan/dsl/ec2.rb | 28 +- lib/ironfan/dsl/openstack.rb | 147 +++++++ lib/ironfan/dsl/security_group.rb | 29 ++ lib/ironfan/dsl/server.rb | 4 +- lib/ironfan/dsl/static.rb | 63 +++ lib/ironfan/dsl/vsphere.rb | 1 + lib/ironfan/headers.rb | 19 + lib/ironfan/provider.rb | 2 + lib/ironfan/provider/ec2/machine.rb | 8 +- lib/ironfan/provider/ec2/security_group.rb | 95 +++-- lib/ironfan/provider/openstack.rb | 69 ++++ lib/ironfan/provider/openstack/elastic_ip.rb | 96 +++++ lib/ironfan/provider/openstack/keypair.rb | 78 ++++ lib/ironfan/provider/openstack/machine.rb | 371 ++++++++++++++++++ .../provider/openstack/security_group.rb | 224 +++++++++++ lib/ironfan/provider/static.rb | 23 ++ lib/ironfan/provider/static/machine.rb | 192 +++++++++ lib/ironfan/requirements.rb | 12 + 27 files changed, 1646 insertions(+), 106 deletions(-) create mode 100644 Gemfile.lock create mode 100644 lib/ironfan/dsl/openstack.rb create mode 100644 lib/ironfan/dsl/security_group.rb create mode 100644 lib/ironfan/dsl/static.rb create mode 100644 lib/ironfan/provider/openstack.rb create mode 100644 lib/ironfan/provider/openstack/elastic_ip.rb create mode 100644 lib/ironfan/provider/openstack/keypair.rb create mode 100644 lib/ironfan/provider/openstack/machine.rb create mode 100644 lib/ironfan/provider/openstack/security_group.rb create mode 100644 lib/ironfan/provider/static.rb create mode 100644 lib/ironfan/provider/static/machine.rb diff --git a/Gemfile b/Gemfile index 4174cd8e..ae6d926c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,6 @@ source 'https://rubygems.org' + gemspec group :development do diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..8de08d4e --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,198 @@ +GEM + remote: http://rubygems.org/ + specs: + archive-tar-minitar (0.5.2) + builder (3.2.2) + bunny (0.7.9) + chef (10.26.0) + bunny (>= 0.6.0, < 0.8.0) + erubis + highline (>= 1.6.9) + json (>= 1.4.4, <= 1.7.7) + mixlib-authentication (>= 1.3.0) + mixlib-cli (>= 1.1.0) + mixlib-config (>= 1.1.2) + mixlib-log (>= 1.3.0) + mixlib-shellout + moneta (< 0.7.0) + net-ssh (~> 2.6) + net-ssh-multi (~> 1.1.0) + ohai (>= 0.6.0) + rest-client (>= 1.0.4, < 1.7.0) + treetop (~> 1.4.9) + uuidtools + yajl-ruby (~> 1.1) + chef-zero (1.5.1) + hashie (~> 2.0) + json + mixlib-log (~> 1.3) + moneta (< 0.7.0) + puma (~> 1.6) + coderay (1.0.9) + columnize (0.3.6) + configliere (0.4.18) + highline (>= 1.5.2) + multi_json (>= 1.1) + diff-lcs (1.2.5) + erubis (2.7.0) + excon (0.31.0) + ffi (1.9.0) + fog (1.19.0) + builder + excon (~> 0.31.0) + formatador (~> 0.2.0) + mime-types + multi_json (~> 1.0) + net-scp (~> 1.1) + net-ssh (>= 2.1.3) + nokogiri (~> 1.5) + ruby-hmac + formatador (0.2.4) + git (1.2.5) + gorillib (0.5.0) + configliere (>= 0.4.13) + json + multi_json (>= 1.1) + guard (1.8.1) + formatador (>= 0.2.4) + listen (>= 1.0.0) + lumberjack (>= 1.0.2) + pry (>= 0.9.10) + thor (>= 0.14.6) + guard-rspec (3.0.2) + guard (>= 1.8) + rspec (~> 2.13) + guard-yard (2.1.0) + guard (>= 1.1.0) + yard (>= 0.7.0) + hashie (2.0.5) + highline (1.6.19) + ipaddress (0.8.0) + jeweler (1.8.4) + bundler (~> 1.0) + git (>= 1.2.5) + rake + rdoc + json (1.5.4) + linecache19 (0.5.12) + ruby_core_source (>= 0.1.4) + listen (1.2.2) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + rb-kqueue (>= 0.2) + lumberjack (1.0.3) + method_source (0.8.1) + mime-types (2.0) + mini_portile (0.5.2) + mixlib-authentication (1.3.0) + mixlib-log + mixlib-cli (1.3.0) + mixlib-config (1.1.2) + mixlib-log (1.6.0) + mixlib-shellout (1.1.0) + moneta (0.6.0) + multi_json (1.8.2) + net-scp (1.1.2) + net-ssh (>= 2.6.5) + net-ssh (2.7.0) + net-ssh-gateway (1.2.0) + net-ssh (>= 2.6.5) + net-ssh-multi (1.1) + net-ssh (>= 2.1.4) + net-ssh-gateway (>= 0.99.0) + nokogiri (1.6.1) + mini_portile (~> 0.5.0) + ohai (6.16.0) + ipaddress + mixlib-cli + mixlib-config + mixlib-log + mixlib-shellout + systemu + yajl-ruby + oj (2.1.2) + polyglot (0.3.3) + pry (0.9.12.2) + coderay (~> 1.0.5) + method_source (~> 0.8) + slop (~> 3.4) + puma (1.6.3) + rack (~> 1.2) + rack (1.5.2) + rake (10.1.0) + rb-fsevent (0.9.3) + rb-inotify (0.9.0) + ffi (>= 0.5.0) + rb-kqueue (0.2.0) + ffi (>= 0.5.0) + rbvmomi (1.6.0) + builder + nokogiri (>= 1.4.1) + trollop + rdoc (4.0.1) + json (~> 1.4) + redcarpet (2.3.0) + rest-client (1.6.7) + mime-types (>= 1.16) + rspec (2.14.1) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) + rspec-core (2.14.7) + rspec-expectations (2.14.4) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.4) + ruby-debug-base19 (0.11.25) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby_core_source (>= 0.1.4) + ruby-debug19 (0.11.6) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby-debug-base19 (>= 0.11.19) + ruby-hmac (0.4.0) + ruby_core_source (0.1.5) + archive-tar-minitar (>= 0.5.2) + ruby_gntp (0.3.4) + simplecov (0.7.1) + multi_json (~> 1.0) + simplecov-html (~> 0.7.1) + simplecov-html (0.7.1) + slop (3.4.5) + systemu (2.5.2) + thor (0.18.1) + treetop (1.4.14) + polyglot + polyglot (>= 0.3.1) + trollop (2.0) + uuidtools (2.1.4) + yajl-ruby (1.1.0) + yard (0.8.6.1) + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 1.0) + chef (~> 10.16) + chef-zero + diff-lcs (~> 1.2.5) + excon (~> 0.31.0) + fog (~> 1.19) + formatador (~> 0.2) + gorillib (~> 0.5.0) + guard (~> 1) + guard-rspec + guard-yard + jeweler (>= 1.6) + json (= 1.5.4) + oj (>= 1.2) + pry + rake + rbvmomi + redcarpet (>= 2.1) + rspec (~> 2.8) + ruby-debug19 + ruby_gntp + simplecov (>= 0.5) + yard (>= 0.7) diff --git a/ironfan.gemspec b/ironfan.gemspec index 907f54d5..dcea2b26 100644 --- a/ironfan.gemspec +++ b/ironfan.gemspec @@ -29,4 +29,5 @@ Gem::Specification.new do |gem| gem.add_dependency('json', '1.5.4') gem.add_development_dependency('bundler', '~> 1.0') + end diff --git a/lib/chef/knife/bootstrap/ubuntu12.04-ironfan.erb b/lib/chef/knife/bootstrap/ubuntu12.04-ironfan.erb index 4c04726f..2aa55f18 100644 --- a/lib/chef/knife/bootstrap/ubuntu12.04-ironfan.erb +++ b/lib/chef/knife/bootstrap/ubuntu12.04-ironfan.erb @@ -14,6 +14,13 @@ set -e <%= (@config[:verbosity].to_i > 1 ? 'set -v' : '') %> +echo "deb http://apt.opscode.com/ `lsb_release -cs`-0.10 main" | sudo tee /etc/apt/sources.list.d/opscode.list + +# Make sure that opscode chef is on the apt repo list. +sudo mkdir -p /etc/apt/trusted.gpg.d +gpg --keyserver keys.gnupg.net --recv-keys 83EF826A +gpg --export packages@opscode.com | sudo tee /etc/apt/trusted.gpg.d/opscode-keyring.gpg > /dev/null + date > /etc/box_build_time echo -e "`date` \n\n**** \n**** apt update:\n****\n" diff --git a/lib/chef/knife/cluster_launch.rb b/lib/chef/knife/cluster_launch.rb index dace454e..b6e849af 100644 --- a/lib/chef/knife/cluster_launch.rb +++ b/lib/chef/knife/cluster_launch.rb @@ -55,6 +55,12 @@ class ClusterLaunch < Knife :boolean => true, :default => false + option :wait_ssh, + :long => "--[no-]wait-ssh", + :description => "Wait for the target machine to open an ssh port", + :boolean => true, + :default => true + def _run load_ironfan die(banner) if @name_args.empty? @@ -110,10 +116,10 @@ def _run def perform_after_launch_tasks(computer) # Try SSH unless config[:dry_run] - Ironfan.step(computer.name, 'trying ssh', :white) - # FIXME: This is EC2-specific, abstract it - address = computer.machine.vpc_id.nil? ? computer.machine.public_hostname : computer.machine.public_ip_address - nil until tcp_test_ssh(address){ sleep @initial_sleep_delay ||= 10 } + if config[:wait_ssh] + Ironfan.step(computer.name, 'trying ssh', :white) + nil until wait_for_ssh(computer){ sleep @initial_sleep_delay ||= 10 } + end end # Run Bootstrap @@ -123,31 +129,6 @@ def perform_after_launch_tasks(computer) end end - def tcp_test_ssh(target) - tcp_socket = TCPSocket.new(target, 22) - readable = IO.select([tcp_socket], nil, nil, 5) - if readable - Chef::Log.debug("sshd accepting connections on #{target}, banner is #{tcp_socket.gets}") - yield - true - else - false - end - rescue Errno::ETIMEDOUT - Chef::Log.debug("ssh to #{target} timed out") - false - rescue Errno::ECONNREFUSED - Chef::Log.debug("ssh connection to #{target} refused") - sleep 2 - false - rescue Errno::EHOSTUNREACH - Chef::Log.debug("ssh host #{target} unreachable") - sleep 2 - false - ensure - tcp_socket && tcp_socket.close - end - def warn_or_die_on_bogus_servers(target) ui.info("") ui.info "Cluster has servers in a transitional or undefined state (shown as 'bogus'):" diff --git a/lib/chef/knife/ironfan_knife_common.rb b/lib/chef/knife/ironfan_knife_common.rb index 61b7d086..08134226 100644 --- a/lib/chef/knife/ironfan_knife_common.rb +++ b/lib/chef/knife/ironfan_knife_common.rb @@ -185,6 +185,41 @@ def run_bootstrap(computer) end end + def wait_for_ssh(computer) + ssh = Chef::Knife::Ssh.new + ssh.ui = ui + ssh.name_args = [ computer.name, "ls" ] + ssh.config[:ssh_user] = Chef::Config[:knife][: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[:ssh_gateway] = Chef::Config[:knife][:ssh_gateway] || config[:ssh_gateway] + ssh.config[:forward_agent] = Chef::Config[:knife][:forward_agent] || config[:forward_agent] + ssh.config[:identity_file] = Chef::Config[:knife][:identity_file] || config[:identity_file] + ssh.config[:manual] = true + ssh.config[:host_key_verify] = Chef::Config[:knife][:host_key_verify] || config[:host_key_verify] + ssh.config[:on_error] = :raise + session = ssh.session + return true + rescue Errno::ETIMEDOUT + Chef::Log.debug("ssh to #{computer.name} timed out") + return false + rescue Errno::ECONNREFUSED + Chef::Log.debug("ssh connection to #{computer.name} refused") + yield + return false + rescue Errno::EHOSTUNREACH + Chef::Log.debug("ssh host #{computer.name} unreachable") + yield + return false + rescue + Chef::Log.debug("something else went wrong while wating for ssh host #{computer.name}") + raise + return false + else + session && session.close + session = nil + end + # # Utilities # diff --git a/lib/ironfan/dsl/cloud.rb b/lib/ironfan/dsl/cloud.rb index f2d76ba5..62d69191 100644 --- a/lib/ironfan/dsl/cloud.rb +++ b/lib/ironfan/dsl/cloud.rb @@ -14,6 +14,8 @@ def self.receive(obj, &block) when :virtualbox then VirtualBox when :vsphere then Vsphere when :rds then Rds + when :openstack then OpenStack + when :static then Static else raise "Unsupported cloud #{obj[:name]}" end end diff --git a/lib/ironfan/dsl/component.rb b/lib/ironfan/dsl/component.rb index 8861f375..64d2608a 100644 --- a/lib/ironfan/dsl/component.rb +++ b/lib/ironfan/dsl/component.rb @@ -110,14 +110,11 @@ def wire_to(compute, full_server_cluster_v, keys) discovery = {discovers: keys.reverse.inject(full_server_cluster_v){|hsh,key| {key => hsh}}} (compute.facet_role || compute.cluster_role).override_attributes(discovery) - # FIXME: This is Ec2-specific and probably doesn't belong here. client_group_v = client_group(compute) server_group_v = security_group(full_server_cluster_v) - if create_security_groups - group_edge(compute.cloud(:ec2), client_group_v, :authorized_by_group, server_group_v) - group_edge(compute.cloud(:ec2), client_group_v, :authorize_group, server_group_v) if bidirectional - end + group_edge(compute, client_group_v, :authorized_by_group, server_group_v) + group_edge(compute, client_group_v, :authorize_group, server_group_v) if bidirectional Chef::Log.debug("discovered #{announce_name} for #{cluster_name}: #{discovery}") end diff --git a/lib/ironfan/dsl/compute.rb b/lib/ironfan/dsl/compute.rb index 605fa4ea..95510b28 100644 --- a/lib/ironfan/dsl/compute.rb +++ b/lib/ironfan/dsl/compute.rb @@ -22,6 +22,7 @@ class Compute < Ironfan::Dsl collection :run_list_items, RunListItem, :resolver => :merge_resolve, :key_method => :name collection :clouds, Ironfan::Dsl::Cloud, :resolver => :merge_resolve, :key_method => :name collection :volumes, Ironfan::Dsl::Volume, :resolver => :merge_resolve, :key_method => :name + collection :security_groups, Ironfan::Dsl::SecurityGroup, :resolver => :merge_resolve, :key_method => :name # Resolve these normally (overriding on each layer) magic :environment, Symbol, :default => :_default diff --git a/lib/ironfan/dsl/ec2.rb b/lib/ironfan/dsl/ec2.rb index d1fb5842..85abaa82 100644 --- a/lib/ironfan/dsl/ec2.rb +++ b/lib/ironfan/dsl/ec2.rb @@ -30,12 +30,13 @@ class Ec2 < Cloud magic :auto_elastic_ip, String magic :allocation_id, String magic :region, String, :default => ->{ default_region } - collection :security_groups, Ironfan::Dsl::Ec2::SecurityGroup, :key_method => :name + collection :security_groups, Ironfan::Dsl::SecurityGroup, :key_method => :name magic :ssh_user, String, :default => ->{ image_info[:ssh_user] } magic :ssh_identity_dir, String, :default => ->{ Chef::Config.ec2_key_dir } magic :subnet, String magic :validation_key, String, :default => ->{ IO.read(Chef::Config.validation_key) rescue '' } magic :vpc, String + magic :dns_search_domain, String, :default => 'internal' def domain; vpc.nil? ? 'standard' : 'vpc'; end @@ -125,31 +126,6 @@ def receive_provider(obj) end end - class SecurityGroup < Ironfan::Dsl - field :name, String - field :group_authorized, Array, :default => [] - field :group_authorized_by, Array, :default => [] - field :range_authorizations, Array, :default => [] - - def authorize_port_range(range, cidr_ip = '0.0.0.0/0', ip_protocol = 'tcp') - range = (range .. range) if range.is_a?(Integer) - range_authorizations << [range, cidr_ip, ip_protocol] - range_authorizations.compact! - range_authorizations.uniq! - end - - def authorized_by_group(other_name) - group_authorized_by << other_name.to_s - group_authorized_by.compact! - group_authorized_by.uniq! - end - - def authorize_group(other_name) - group_authorized << other_name.to_s - group_authorized.compact! - group_authorized.uniq! - end - end class ElasticLoadBalancer diff --git a/lib/ironfan/dsl/openstack.rb b/lib/ironfan/dsl/openstack.rb new file mode 100644 index 00000000..017fd9cc --- /dev/null +++ b/lib/ironfan/dsl/openstack.rb @@ -0,0 +1,147 @@ +require 'digest/md5' + +module Ironfan + class Dsl + + class Compute < Ironfan::Dsl + def openstack(*attrs,&block) cloud(:openstack,*attrs,&block); end + end + + class OpenStack < Cloud + magic :availability_zones, Array, :default => ['nova'] + magic :backing, String, :default => 'ebs' + magic :bits, Integer, :default => ->{ flavor_info[:bits] } + magic :bootstrap_distro, String, :default => ->{ image_info[:bootstrap_distro] } + magic :chef_client_script, String + magic :default_availability_zone, String, :default => ->{ availability_zones.first } + #collection :elastic_load_balancers, Ironfan::Dsl::Ec2::ElasticLoadBalancer, :key_method => :name + magic :ebs_optimized, :boolean, :default => false + magic :flavor, String, :default => 't1.micro' + #collection :iam_server_certificates, Ironfan::Dsl::Ec2::IamServerCertificate, :key_method => :name + magic :image_id, String + magic :image_name, String + magic :keypair, String + magic :monitoring, String + magic :mount_ephemerals, Hash, :default => {} + magic :permanent, :boolean, :default => false + magic :placement_group, String + magic :provider, Whatever, :default => Ironfan::Provider::OpenStack + magic :elastic_ip, String + magic :auto_elastic_ip, String + magic :allocation_id, String + magic :region, String, :default => ->{ default_region } + collection :security_groups, Ironfan::Dsl::SecurityGroup, :key_method => :name + magic :ssh_user, String, :default => ->{ image_info[:ssh_user] } + magic :ssh_identity_dir, String, :default => ->{ Chef::Config.openstack_key_dir } + magic :subnet, String + magic :validation_key, String, :default => ->{ IO.read(Chef::Config.validation_key) rescue '' } + magic :vpc, String + magic :dns_search_domain, String, :default => 'novalocal' + + def domain; vpc.nil? ? 'standard' : 'vpc'; end + + def image_info + bit_str = "#{self.bits.to_i}-bit" # correct for legacy image info. + keys = [region, bit_str, backing, image_name] + info = Chef::Config[:openstack_image_info][ keys ] + ui.warn("Can't find image for #{[region, bit_str, backing, image_name].inspect}") if info.blank? + return info || {} + end + + def image_id + result = read_attribute(:image_id) || image_info[:image_id] + end + + def ssh_key_name(computer) + keypair ? keypair.to_s : computer.server.cluster_name + end + + def default_region + default_availability_zone ? default_availability_zone.gsub(/^(\w+-\w+-\d)[a-z]/, '\1') : nil + end + + def to_display(style,values={}) + return values if style == :minimal + + values["Flavor"] = flavor + values["AZ"] = default_availability_zone + return values if style == :default + + values["Public IP"] = elastic_ip if elastic_ip + values + end + + def flavor_info + if not Chef::Config[:openstack_flavor_info].has_key?(flavor) + ui.warn("Unknown machine image flavor '#{flavor}'") + list_flavors + return nil + end + Chef::Config[:openstack_flavor_info][flavor] + end + + def implied_volumes + result = [] + if backing == 'ebs' + result << Ironfan::Dsl::Volume.new(:name => 'root') do + device '/dev/sda1' + fstype 'ext4' + keep false + mount_point '/' + end + end + return result unless (mount_ephemerals and (flavor_info[:ephemeral_volumes] > 0)) + + layout = { 0 => ['/dev/sdb','/mnt'], + 1 => ['/dev/sdc','/mnt2'], + 2 => ['/dev/sdd','/mnt3'], + 3 => ['/dev/sde','/mnt4'] } + ( 0 .. (flavor_info[:ephemeral_volumes]-1) ).each do |idx| + dev, mnt = layout[idx] + ephemeral = Ironfan::Dsl::Volume.new(:name => "ephemeral#{idx}") do + attachable 'ephemeral' + fstype 'ext3' + device dev + mount_point mnt + mount_options 'defaults,noatime' + tags({:bulk => true, :local => true, :fallback => true}) + end + ephemeral_attrs = mount_ephemerals.clone + if ephemeral_attrs.has_key?(:disks) + disk_attrs = mount_ephemerals[:disks][idx] || { } + ephemeral_attrs.delete(:disks) + ephemeral_attrs.merge!(disk_attrs) + end + ephemeral.receive! ephemeral_attrs + result << ephemeral + end + result + end + + def receive_provider(obj) + if obj.is_a?(String) + write_attribute :provider, Gorillib::Inflector.constantize(Gorillib::Inflector.camelize(obj.gsub(/\./, '/'))) + else + super(obj) + end + end + end + end +end + +Chef::Config[:openstack_flavor_info] ||= {} +Chef::Config[:openstack_flavor_info].merge!({ + # 32-or-64: m1.small, m1.medium, t1.micro, c1.medium + 't1.micro' => { :price => 0.02, :bits => 64, :ram => 686, :cores => 1, :core_size => 0.25, :inst_disks => 0, :inst_disk_size => 0, :ephemeral_volumes => 0 }, + 'm1.small' => { :price => 0.08, :bits => 64, :ram => 1740, :cores => 1, :core_size => 1, :inst_disks => 1, :inst_disk_size => 160, :ephemeral_volumes => 1 }, + 'm1.medium' => { :price => 0.165, :bits => 64, :ram => 3840, :cores => 2, :core_size => 1, :inst_disks => 1, :inst_disk_size => 410, :ephemeral_volumes => 1 }, + 'c1.medium' => { :price => 0.17, :bits => 64, :ram => 1740, :cores => 2, :core_size => 2.5, :inst_disks => 1, :inst_disk_size => 350, :ephemeral_volumes => 1 }, + # + 'm1.large' => { :price => 0.32, :bits => 64, :ram => 7680, :cores => 2, :core_size => 2, :inst_disks => 1, :inst_disk_size => 850, :ephemeral_volumes => 2, }, + 'm2.xlarge' => { :price => 0.45, :bits => 64, :ram => 18124, :cores => 2, :core_size => 3.25, :inst_disks => 1, :inst_disk_size => 420, :ephemeral_volumes => 1, }, + 'c1.xlarge' => { :price => 0.64, :bits => 64, :ram => 7168, :cores => 8, :core_size => 2.5, :inst_disks => 1, :inst_disk_size => 1690, :ephemeral_volumes => 4, }, + 'm1.xlarge' => { :price => 0.66, :bits => 64, :ram => 15360, :cores => 4, :core_size => 2, :inst_disks => 1, :inst_disk_size => 1690, :ephemeral_volumes => 4, }, + }) + +Chef::Config[:openstack_image_info] ||= {} + diff --git a/lib/ironfan/dsl/security_group.rb b/lib/ironfan/dsl/security_group.rb new file mode 100644 index 00000000..9cc8e08d --- /dev/null +++ b/lib/ironfan/dsl/security_group.rb @@ -0,0 +1,29 @@ +module Ironfan + class Dsl + class SecurityGroup < Ironfan::Dsl + field :name, String + field :group_authorized, Array, :default => [] + field :group_authorized_by, Array, :default => [] + field :range_authorizations, Array, :default => [] + + def authorize_port_range(range, cidr_ip = '0.0.0.0/0', ip_protocol = 'tcp') + range = (range .. range) if range.is_a?(Integer) + range_authorizations << [range, cidr_ip, ip_protocol] + range_authorizations.compact! + range_authorizations.uniq! + end + + def authorized_by_group(other_name) + group_authorized_by << other_name.to_s + group_authorized_by.compact! + group_authorized_by.uniq! + end + + def authorize_group(other_name) + group_authorized << other_name.to_s + group_authorized.compact! + group_authorized.uniq! + end + end + end +end diff --git a/lib/ironfan/dsl/server.rb b/lib/ironfan/dsl/server.rb index e6de5eae..aae354c8 100644 --- a/lib/ironfan/dsl/server.rb +++ b/lib/ironfan/dsl/server.rb @@ -279,10 +279,10 @@ def to_machine_manifest bootstrap_distro: cloud.bootstrap_distro, chef_client_script: cloud.chef_client_script, default_availability_zone: cloud.default_availability_zone, - elastic_load_balancers: cloud.elastic_load_balancers, + elastic_load_balancers: cloud.respond_to?(:elastic_load_balancers) ? cloud.elastic_load_balancers : nil, ebs_optimized: cloud.ebs_optimized, flavor: cloud.flavor, - iam_server_certificates: cloud.iam_server_certificates, + iam_server_certificates: cloud.respond_to?(:iam_server_certificates) ? cloud.iam_server_certificates : nil, image_id: cloud.image_id, image_name: cloud.image_name, keypair: cloud.keypair, diff --git a/lib/ironfan/dsl/static.rb b/lib/ironfan/dsl/static.rb new file mode 100644 index 00000000..4a98ceea --- /dev/null +++ b/lib/ironfan/dsl/static.rb @@ -0,0 +1,63 @@ +require 'digest/md5' + +module Ironfan + class Dsl + + class Compute < Ironfan::Dsl + def static(*attrs,&block) cloud(:static,*attrs,&block); end + end + + class Static < Cloud + + magic :private_ip, String + magic :public_ip, String + magic :private_hostname, String + magic :public_hostname, String + magic :keypair, String + magic :provider, Whatever, :default => Ironfan::Provider::Static + + magic :availability_zones, Array, :default => [] + magic :backing, String, :default => nil + magic :bits, Integer, :default => 64 + magic :bootstrap_distro, String, :default => "ubuntu12.04-ironfan" + magic :chef_client_script, String + magic :default_availability_zone, String, :default => "none" + magic :ebs_optimized, :boolean, :default => false + magic :flavor, String, :default => 'whatever' + magic :image_id, String + magic :image_name, String + magic :monitoring, String + magic :mount_ephemerals, Hash, :default => {} + magic :permanent, :boolean, :default => false + magic :placement_group, String + magic :elastic_ip, String + magic :auto_elastic_ip, String + magic :allocation_id, String + magic :region, String, :default => 'none' + collection :security_groups, Ironfan::Dsl::SecurityGroup, :key_method => :name + magic :ssh_user, String, :default => 'ubuntu' + magic :ssh_identity_dir, String, :default => ->{ Chef::Config.openstack_key_dir } + magic :subnet, String + magic :validation_key, String, :default => ->{ IO.read(Chef::Config.validation_key) rescue '' } + magic :vpc, String + magic :dns_search_domain, String, :default => 'static' + + def to_display(style,values={}) + return values if style == :minimal + + values["Private IP"] = private_ip + values["Public IP"] = public_ip + values + end + + def flavor_info + return nil + end + + def implied_volumes + [] + end + end + end +end + diff --git a/lib/ironfan/dsl/vsphere.rb b/lib/ironfan/dsl/vsphere.rb index 33f1ee93..c32fb5f5 100644 --- a/lib/ironfan/dsl/vsphere.rb +++ b/lib/ironfan/dsl/vsphere.rb @@ -32,6 +32,7 @@ class Vsphere < Cloud magic :virtual_disks, Array, :default => [] magic :vsphere_datacenters, Array, :default => ['New Datacenter'] magic :network, String, :default => "VM Network" + magic :dns_search_domain, String, :default => 'internal' def image_info bit_str = "#{self.bits.to_i}-bit" # correct for legacy image info. diff --git a/lib/ironfan/headers.rb b/lib/ironfan/headers.rb index b520b385..1c43dbbb 100644 --- a/lib/ironfan/headers.rb +++ b/lib/ironfan/headers.rb @@ -31,6 +31,14 @@ class SecurityGroup < Ironfan::Dsl; end class ElasticLoadBalancer < Ironfan::Dsl; end class IamServerCertificate < Ironfan::Dsl; end end + + class OpenStack < Cloud + class SecurityGroup < Ironfan::Dsl; end + end + + class Static < Cloud + end + class VirtualBox < Cloud; end class Vsphere < Cloud; end class Rds < Cloud @@ -65,6 +73,17 @@ class SecurityGroup < Ironfan::Provider::Resource; end class ElasticLoadBalancer < Ironfan::Provider::Resource; end class IamServerCertificate < Ironfan::Provider::Resource; end end + + class OpenStack < Ironfan::IaasProvider + class Machine < Ironfan::IaasProvider::Machine; end + class Keypair < Ironfan::Provider::Resource; end + class SecurityGroup < Ironfan::Provider::Resource; end + end + + class Static < Ironfan::IaasProvider + class Machine < Ironfan::IaasProvider::Machine; end + end + class VirtualBox < Ironfan::IaasProvider class Machine < Ironfan::IaasProvider::Machine; end end diff --git a/lib/ironfan/provider.rb b/lib/ironfan/provider.rb index b8469422..e3085717 100644 --- a/lib/ironfan/provider.rb +++ b/lib/ironfan/provider.rb @@ -13,6 +13,8 @@ def self.receive(obj, &block) case obj[:name] when :chef then Chef when :ec2 then Ec2 + when :openstack then OpenStack + when :static then Static when :vsphere then Vsphere when :virtualbox then VirtualBox when :rds then Rds diff --git a/lib/ironfan/provider/ec2/machine.rb b/lib/ironfan/provider/ec2/machine.rb index a1fd6cc4..d3948e1e 100644 --- a/lib/ironfan/provider/ec2/machine.rb +++ b/lib/ironfan/provider/ec2/machine.rb @@ -256,9 +256,13 @@ def self.launch_description(computer) } # VPC security_groups can only be addressed by id (not name) - description[:security_group_ids] = cloud.security_groups.keys.map do |g| - SecurityGroup.recall( SecurityGroup.group_name_with_vpc(g,cloud.vpc) ).group_id + sec_group_ids = [] + [computer.server.security_groups, cloud.security_groups].each do |container| + sec_group_ids += container.security_groups.keys.map do |g| + SecurityGroup.recall( SecurityGroup.group_name_with_vpc(g,cloud.vpc) ).group_id + end end + description[:security_group_ids] = sec_group_ids.uniq description[:iam_server_certificates] = cloud.iam_server_certificates.values.map do |cert| IamServerCertificate.recall(IamServerCertificate.full_name(computer, cert)) diff --git a/lib/ironfan/provider/ec2/security_group.rb b/lib/ironfan/provider/ec2/security_group.rb index 0e61152e..49dcecd0 100644 --- a/lib/ironfan/provider/ec2/security_group.rb +++ b/lib/ironfan/provider/ec2/security_group.rb @@ -21,7 +21,16 @@ def self.resource_type() :security_group; end def self.expected_ids(computer) return unless computer.server ec2 = computer.server.cloud(:ec2) - ec2.security_groups.keys.map { |name| group_name_with_vpc(name,ec2.vpc) }.uniq + + server_groups = computer.server.security_groups + cloud_groups = ec2.security_groups + + result = [] + [server_groups, cloud_groups].each do |container| + container.keys.each { |name| result.push( group_name_with_vpc(name,ec2.vpc) )} + end + return result.uniq + end def name() @@ -83,44 +92,46 @@ def self.prepare!(computers) # Iterate over all of the security group information, keeping track of # any groups that must exist and any authorizations that must be ensured - cloud.security_groups.values.each do |dsl_group| - - groups_to_create << dsl_group.name - - groups_to_create << dsl_group.group_authorized.map do |other_group| - most_appropriate_group_name(other_group, cloud.vpc) - end - - groups_to_create << dsl_group.group_authorized_by.map do |other_group| - most_appropriate_group_name(other_group, cloud.vpc) - end - - authorizations_to_ensure << dsl_group.group_authorized.map do |other_group| - { - :grantor => most_appropriate_group_name(dsl_group.name, cloud.vpc), - :grantee => most_appropriate_group_name(other_group, cloud.vpc), - :grantee_type => :group, - :range => WIDE_OPEN, - } - end - - authorizations_to_ensure << dsl_group.group_authorized_by.map do |other_group| - { - :grantor => most_appropriate_group_name(other_group, cloud.vpc), - :grantee => most_appropriate_group_name(dsl_group.name, cloud.vpc), - :grantee_type => :group, - :range => WIDE_OPEN, - } - end - - authorizations_to_ensure << dsl_group.range_authorizations.map do |range_auth| - range, cidr, protocol = range_auth - { - :grantor => group_name_with_vpc(dsl_group.name, cloud.vpc), - :grantee => { :cidr_ip => cidr, :ip_protocol => protocol }, - :grantee_type => :cidr, - :range => range, - } + [computer.server.security_groups, cloud.security_groups].each do |container| + container.values.each do |dsl_group| + + groups_to_create << dsl_group.name + + groups_to_create << dsl_group.group_authorized.map do |other_group| + most_appropriate_group_name(other_group, cloud.vpc) + end + + groups_to_create << dsl_group.group_authorized_by.map do |other_group| + most_appropriate_group_name(other_group, cloud.vpc) + end + + authorizations_to_ensure << dsl_group.group_authorized.map do |other_group| + { + :grantor => most_appropriate_group_name(dsl_group.name, cloud.vpc), + :grantee => most_appropriate_group_name(other_group, cloud.vpc), + :grantee_type => :group, + :range => WIDE_OPEN, + } + end + + authorizations_to_ensure << dsl_group.group_authorized_by.map do |other_group| + { + :grantor => most_appropriate_group_name(other_group, cloud.vpc), + :grantee => most_appropriate_group_name(dsl_group.name, cloud.vpc), + :grantee_type => :group, + :range => WIDE_OPEN, + } + end + + authorizations_to_ensure << dsl_group.range_authorizations.map do |range_auth| + range, cidr, protocol = range_auth + { + :grantor => group_name_with_vpc(dsl_group.name, cloud.vpc), + :grantee => { :cidr_ip => cidr, :ip_protocol => protocol }, + :grantee_type => :cidr, + :range => range, + } + end end end end @@ -186,11 +197,11 @@ def self.ensure_groups(computer) # FIXME: This violates the DSL's immutability; it should be # something calculated from within the DSL construction Ironfan.todo("CODE SMELL: violation of DSL immutability: #{caller}") - cloud = computer.server.cloud(:ec2) - c_group = cloud.security_group(computer.server.cluster_name) + server = computer.server + c_group = server.security_group(computer.server.cluster_name) c_group.authorized_by_group(c_group.name) facet_name = "#{computer.server.cluster_name}-#{computer.server.facet_name}" - cloud.security_group(facet_name) + server.security_group(facet_name) end # Try an authorization, ignoring duplicates (this is easier than correlating). diff --git a/lib/ironfan/provider/openstack.rb b/lib/ironfan/provider/openstack.rb new file mode 100644 index 00000000..a1cd8e05 --- /dev/null +++ b/lib/ironfan/provider/openstack.rb @@ -0,0 +1,69 @@ +module Ironfan + class Provider + + class OpenStack < Ironfan::IaasProvider + self.handle = :openstack + + def self.resources + [ Machine, Keypair, SecurityGroup, ElasticIp ] + end + + # + # Utility functions + # + def self.connection + @@connection ||= Fog::Compute.new(self.openstack_credentials.merge({ :provider => 'openstack' })) + end + + + # + # Returns a hash that maps flavor names to flavors + # + def self.flavor_hash + @@flavors ||= self.connection.flavors.inject({}){|h,f| h[f.name]=f; h } + end + + # + # Returns a hash that maps flavor ids to flavors + # + def self.flavor_id_hash + @@flavor_ids ||= self.connection.flavors.inject({}){|h,f| h[f.id]=f; h } + end + + + # Ensure that a fog object (machine, volume, etc.) has the proper tags on it + def self.ensure_tags(tags,fog) + # openstack does not have tags. + + #tags.delete_if {|k, v| fog.tags[k] == v.to_s rescue false } + #return if tags.empty? + + #Ironfan.step(fog.name,"tagging with #{tags.inspect}", :green) + #tags.each do |k, v| + # Chef::Log.debug( "tagging #{fog.name} with #{k} = #{v}" ) + # Ironfan.safely do + # config = {:key => k, :value => v.to_s, :resource_id => fog.id } + # connection.tags.create(config) + # end + #end + end + + def self.applicable(computer) + computer.server and computer.server.clouds.include?(:openstack) + end + + private + + def self.openstack_credentials + return { + :openstack_api_key => Chef::Config[:knife][:openstack_api_key], + :openstack_username => Chef::Config[:knife][:openstack_username], + :openstack_auth_url => Chef::Config[:knife][:openstack_auth_url], + :openstack_tenant => Chef::Config[:knife][:openstack_tenant], + :openstack_region => Chef::Config[:knife][:openstack_region], + } + end + + end + end +end diff --git a/lib/ironfan/provider/openstack/elastic_ip.rb b/lib/ironfan/provider/openstack/elastic_ip.rb new file mode 100644 index 00000000..92ec7a41 --- /dev/null +++ b/lib/ironfan/provider/openstack/elastic_ip.rb @@ -0,0 +1,96 @@ +module Ironfan + class Provider + class OpenStack + class ElasticIp < Ironfan::Provider::Resource + delegate :addresses, :associate_address, + :allocate_address, :auto_elastic_ip, :destroy, + :domain, :domain=, :describe_addresses, :disassociate_address, + :domain, :id, :network_interface_id, :network_interface_id=, + :save, :server=, + :server, :server_id, :server_id=, + :to => :adaptee + + def self.shared?() true; end + def self.multiple?() false; end + def self.resource_type() :elastic_ip; end + def self.expected_ids(computer) [ computer.server.openstack.elastic_ip ]; end + + def public_ip() adaptee.ip ; end + def name() adaptee.ip ; end + + # + # Discovery + # + + def self.load!(cluster=nil) + OpenStack.connection.addresses.each do |eip| + register eip + + # The rest of this definition shows relevant information when -VV + # is passed to knife and aids in troubleshooting any refusal to + # attach Elastic IPs + Chef::Log.debug( "OpenStack Pool: #{eip.pool}" ) + if eip.ip.nil? + Chef::Log.debug( "no Elastic IPs currently allocated" ) + else + Chef::Log.debug( "available ip match: #{eip.ip}" ) + Chef::Log.debug( "available allocation_id match: #{eip.id}" ) + end + Chef::Log.debug( "----------------------" ) + end + + cluster.servers.each do |s| + next if s.openstack.elastic_ip.nil? + if recall? s.openstack.elastic_ip + Chef::Log.debug( "Cluster elastic_ip matches #{s.openstack.elastic_ip}" ) + else + Chef::Log.debug( "No matching Elastic IP for #{s.openstack.elastic_ip}" ) + end + end + + end + + # + # Manipulation + # + + def self.save!(computer) + return unless computer.created? + return unless elastic_ip = computer.server.openstack.elastic_ip + return unless recall? elastic_ip + # also, in the case of VPC Elastic IPs, can discover and use allocation_id to attach a VPC Elastic IP. + return unless computer.server.openstack.methods.include?(:elastic_ip) + if ( computer.server.openstack.elastic_ip.nil?) + if computer.server.addresses.nil? + OpenStack.connection.allocate_address + load! + elastic_ip = computer.server.addresses.first.public_ip + Chef::Log.debug( "allocating new Elastic IP address" ) + else + # Second, :elastic_ip is set, has an address available to use but has no set value available in facet definition. + elastic_ip = computer.server.addresses.first.public_ip + Chef::Log.debug( "using first available Elastic IP address" ) + end + elsif ( !computer.server.openstack.elastic_ip.nil? or cloud.vpc.nil? ) + # Third, :elastic_ip is set, has an address available to use, has a set value in facet definition and is not VPC. + elastic_ip = computer.server.openstack.elastic_ip + Chef::Log.debug( "using requested Elastic IP address" ) + elsif ( computer.server.opentsack.elastic_ip.nil? ) + # Fourth, is exactly like Third but on a VPC domain. (this is functionaility for attaching VPC Elastic IPS) + allocation_id = computer.server.openstack.allocation_id + Chef::Log.debug( "using Elastic IP address matched to given Allocation ID" ) + else + ui.fatal("You have set both :elastic_ip and :auto_elastic_ip in your facet definition; which are mutually exclusive.") + end + Ironfan.step(computer.name, "associating Elastic IP #{elastic_ip}", :blue) + Ironfan.unless_dry_run do + Ironfan.safely do + allocation_id = recall(elastic_ip).id + OpenSTack.connection.associate_address( computer.machine.id, elastic_ip, nil, allocation_id ) + end + end + end + end + end + end +end diff --git a/lib/ironfan/provider/openstack/keypair.rb b/lib/ironfan/provider/openstack/keypair.rb new file mode 100644 index 00000000..6c9b8319 --- /dev/null +++ b/lib/ironfan/provider/openstack/keypair.rb @@ -0,0 +1,78 @@ +module Ironfan + class Provider + class OpenStack + + class Keypair < Ironfan::Provider::Resource + delegate :_dump, :collection, :collection=, :connection, + :connection=, :destroy, :fingerprint, :fingerprint=, :identity, + :identity=, :name, :name=, :new_record?, :public_key, + :public_key=, :reload, :requires, :requires_one, :save, + :symbolize_keys, :wait_for, :writable?, :write, + :to => :adaptee + + field :key_filename, String, :default => ->{ "#{Keypair.key_dir}/#{name}.pem" } + + def self.shared? ; true ; end + def self.multiple? ; false ; end + def self.resource_type ; :keypair ; end + def self.expected_ids(computer) + [computer.server.cluster_name] + end + + def private_key + File.open(key_filename, "rb").read + end + + def private_key=(body=nil) + File.open(key_filename, "w", 0600){|f| f.print( body ) } + end + + def to_s + "<%-15s %-12s>" % [self.class.handle, name] + end + + # + # Discovery + # + def self.load!(cluster=nil) + OpenStack.connection.key_pairs.each do |keypair| + register keypair unless keypair.blank? + end + end + + def receive_adaptee(obj) + obj = Openstack.connection.key_pairs.new(obj) if obj.is_a?(Hash) + super + end + + # + # Manipulation + # + + def self.prepare!(computers) + return if computers.empty? + name = computers.values[0].server.cluster_name + return if recall? name + Ironfan.step(name, "creating key pair for #{name}", :blue) + result = OpenStack.connection.create_key_pair(name) + private_key = result.body["keypair"]["private_key"] + load! # Reload to get the native object + recall(name).private_key = private_key + end + + # + # Utility + # + + def self.key_dir + return Chef::Config.openstack_key_dir if Chef::Config.openstack_key_dir + dir = "#{ENV['HOME']}/.chef/credentials/openstack_keys" + warn "Please set 'openstack_key_dir' in your knife.rb. Will use #{dir} as a default" + dir + end + + end + + end + end +end diff --git a/lib/ironfan/provider/openstack/machine.rb b/lib/ironfan/provider/openstack/machine.rb new file mode 100644 index 00000000..4cf78b5e --- /dev/null +++ b/lib/ironfan/provider/openstack/machine.rb @@ -0,0 +1,371 @@ +module Ironfan + class Provider + class OpenStack + + class Machine < Ironfan::IaasProvider::Machine + delegate :_dump, :addresses, :ami_launch_index, :ami_launch_index=, + :architecture, :architecture=, :availability_zone, + :availability_zone=, :block_device_mapping, :block_device_mapping=, + :client_token, :client_token=, :collection, :collection=, + :connection, :connection=, :console_output, + :destroy, + :ebs_optimized, :flavor, :flavor=, + :iam_instance_profile, + :iam_instance_profile=, :iam_instance_profile_arn=, + :iam_instance_profile_name=, :id, :id=, :identity, :identity=, + :image, :image=, :instance_initiated_shutdown_behavior, + :instance_initiated_shutdown_behavior=, :ip_address, :kernel_id, + :kernel_id=, :key_name, :key_name=, :key_pair, :key_pair=, + :monitor=, :monitoring, :monitoring=, :network_interfaces, + :network_interfaces=, :new_record?, :password, :password=, + :placement_group, :placement_group=, :platform, :platform=, + :private_key, :private_key=, + :private_key_path, :private_key_path=, :product_codes, + :product_codes=, + :public_key, :public_key=, :public_key_path, :public_key_path=, + :ramdisk_id, :ramdisk_id=, :ready?, :reason, :reason=, :reboot, + :reload, :requires, :requires_one, :root_device_name, + :root_device_name=, :root_device_type, :root_device_type=, :save, + :scp, :scp_download, :scp_upload, :security_group_ids, + :security_group_ids=, :setup, :ssh, :ssh_port, :sshable?, :start, + :state, :state=, :state_reason, :state_reason=, :stop, :subnet_id, + :subnet_id=, :symbolize_keys, :tenancy, :tenancy=, + :user_data, :user_data=, :username, :username=, :volumes, + :wait_for, :name, :name=, + :metadata, :metadata=, + :to => :adaptee + + def self.shared?() false; end + def self.multiple?() false; end +# def self.resource_type() Ironfan::IaasProvider::Machine; end + def self.resource_type() :machine; end + def self.expected_ids(computer) [computer.server.full_name]; end + + def tags + t = metadata.to_hash.update({"Name" => @adaptee.name}) + return t.keys.inject({}) {|h,k| h[k]=t[k]; h[k.to_sym]=t[k]; h} + end + + def vpc_id + return nil + end + + def created_at + return @adaptee.created + end + + def flavor_id + # sometimes flavor comes back empty - especially right after the machine has been launched + return flavor && flavor["id"] + end + + def flavor_name + fl = OpenStack.flavor_id_hash[ flavor_id ] + fl && fl.name + end + + def image_id + return image[:id] + end + + def groups ; Array(@adaptee.security_groups) ; end + + def public_hostname ; private_ip_address ; end + def dns_name ; public_ip_address ; end + + def keypair ; key_pair ; end + + def created? + not ['HARD_DELETED', 'SOFT_DELETED', ].include? state + end + def pending? + state == "BUILD" + end + def running? + state == "ACTIVE" + end + def stopping? + state == "STOPPING" + end + + def stopped? + state == "STOPPED" + end + + def error? + state == "ERROR" + end + + def start + machine = self + adaptee.start + adaptee.wait_for{ machine.pending? or machine.running? or machine.error? } + end + + def stop + machine = self + adaptee.stop + adaptee.wait_for{ machine.stopping? or machine.stopped? } + end + + def perform_after_launch_tasks? + true + end + + def to_display(style,values={}) + # style == :minimal + values["State"] = (state || "unknown").to_sym + values["MachineID"] = id + values["Public IP"] = public_ip_address + values["Private IP"] = private_ip_address + values["Created On"] = created_at.to_date + return values if style == :minimal + + # style == :default + values["Flavor"] = flavor_name + values["AZ"] = availability_zone + return values if style == :default + + # style == :expanded + values["Image"] = image_id + #values["Volumes"] = volumes.map(&:id).join(', ') + values["SSH Key"] = key_name + values + end + + def ssh_key + keypair = cloud.keypair || computer.server.cluster_name + end + + def private_ip_address + adaptee.private_ip_address rescue nil + end + + def public_ip_address + adaptee.floating_ip_address rescue nil + end + + def to_s + "<%-15s %-12s %-25s %-25s %-15s %-15s %-12s %-12s %s:%s>" % [ + self.class.handle, id, created_at, name, private_ip_address, public_ip_address, flavor_name, availability_zone, key_name, groups.join(',') ] + end + + # + # Discovery + # + def self.load!(cluster=nil) + OpenStack.connection.servers.each do |fs| + machine = new(:adaptee => fs) + if (not machine.created?) + next unless Ironfan.chef_config[:include_terminated] + remember machine, :append_id => "terminated:#{machine.id}" + elsif recall? machine.name + machine.bogus << :duplicate_machines + recall(machine.name).bogus << :duplicate_machines + remember machine, :append_id => "duplicate:#{machine.id}" + else # never seen it + remember machine + end + end + end + + def receive_adaptee(obj) + obj = OpenStack.connection.servers.new(obj) if obj.is_a?(Hash) + super + end + + # Find active machines that haven't matched, but should have, + # make sure all bogus machines have a computer to attach to + # for display purposes + def self.validate_resources!(computers) + recall.each_value do |machine| + next unless machine.users.empty? and machine.name + if machine.name.match("^#{computers.cluster.name}-") + machine.bogus << :unexpected_machine + end + next unless machine.bogus? + fake = Ironfan::Broker::Computer.new + fake[:machine] = machine + fake.name = machine.name + machine.users << fake + computers << fake + end + end + + # + # Manipulation + # + def self.create!(computer) + Ironfan.todo("CODE SMELL: overly large method: #{caller}") + return if computer.machine? and computer.machine.created? + Ironfan.step(computer.name,"creating cloud machine", :green) + # + errors = lint(computer) + if errors.present? then raise ArgumentError, "Failed validation: #{errors.inspect}" ; end + # + launch_desc = launch_description(computer) + Chef::Log.debug(JSON.pretty_generate(launch_desc)) + + # tag the computer correctly + tags = { + 'cluster' => computer.server.cluster_name, + 'facet' => computer.server.facet_name, + 'index' => computer.server.index.to_s, + 'name' => computer.name, + 'creator' => Chef::Config.username + } + + Ironfan.safely do + fog_server = OpenStack.connection.servers.create(launch_desc) + machine = Machine.new(:adaptee => fog_server) + computer.machine = machine + remember machine, :id => computer.name + + Ironfan.step(fog_server.id,"waiting for machine to be ready", :gray) + Ironfan.tell_you_thrice :name => fog_server.id, + :problem => "server unavailable", + :error_class => Fog::Errors::Error do + fog_server.wait_for { state == "ACTIVE" } + end + end + + + computer.machine.metadata.set(tags) + + #OpenStack.ensure_tags(tags, computer.machine) + + # no volumes at the momnt + + # register the new volumes for later save!, and tag appropriately + #computer.machine.volumes.each do |v| + # Ironfan.todo "CODE SMELL: Machine is too familiar with EbsVolume problems" + # ebs_vol = OpenStack::EbsVolume.register v + # drive = computer.drives.values.select do |drive| + # drive.volume.device == ebs_vol.device + # end.first + # drive.disk = ebs_vol + # + # vol_name = "#{computer.name}-#{drive.volume.name}" + # tags['server'] = computer.name + # tags['name'] = vol_name + # tags['Name'] = vol_name + # tags['mount_point'] = drive.volume.mount_point + # tags['device'] = drive.volume.device + # OpenStack.ensure_tags(tags,ebs_vol) + #end + end + + # @returns [Hash{String, Array}] of 'what you did wrong' => [relevant, info] + def self.lint(computer) + cloud = computer.server.cloud(:openstack) + info = [computer.name, cloud.inspect] + errors = {} + server_errors = computer.server.lint + errors["Unhappy Server"] = server_errors if server_errors.present? + errors["No AMI found"] = info if cloud.image_id.blank? + errors['Missing client'] = info unless computer.client? + errors['Missing private_key'] = computer.client unless computer.private_key + # + #all_asserted_regions = [OpenStack.connection.region, cloud.region, Chef::Config[:knife][:region], Ironfan.chef_config[:region]].compact.uniq + #errors["mismatched region"] = all_asserted_regions unless all_asserted_regions.count == 1 + # + errors + end + + def self.launch_description(computer) + cloud = computer.server.cloud(:openstack) + user_data_hsh = { + :chef_server => Chef::Config[:chef_server_url], + :node_name => computer.name, + :organization => Chef::Config[:organization], + :cluster_name => computer.server.cluster_name, + :facet_name => computer.server.facet_name, + :facet_index => computer.server.index, + :client_key => computer.private_key + } + + # main machine info + # note that Fog does not actually create tags when it creates a + # server; they and permanence are applied during sync + description = { + :image_ref => cloud.image_id, + :flavor_ref => OpenStack.flavor_hash[cloud.flavor].id, + #:vpc_id => cloud.vpc, + #:subnet_id => cloud.subnet, + :key_name => cloud.ssh_key_name(computer), + :user_data => JSON.pretty_generate(user_data_hsh), + #:block_device_mapping => block_device_mapping(computer), + :availability_zone => cloud.default_availability_zone, + #:monitoring => cloud.monitoring, + :name => computer.name, + } + + description[:security_groups] = (computer.server.security_groups.keys + cloud.security_groups.keys).uniq + + #description[:iam_server_certificates] = cloud.iam_server_certificates.values.map do |cert| + # IamServerCertificate.recall(IamServerCertificate.full_name(computer, cert)) + #end.compact.map(&:name) + + #description[:elastic_load_balancers] = cloud.elastic_load_balancers.values.map do |elb| + # ElasticLoadBalancer.recall(ElasticLoadBalancer.full_name(computer, elb)) + #end.compact.map(&:name) + + #if cloud.flavor_info[:placement_groupable] + # ui.warn "1.3.1 and earlier versions of Fog don't correctly support placement groups, so your nodes will land willy-nilly. We're working on a fix" + # description[:placement] = { 'groupName' => cloud.placement_group.to_s } + #end + #if cloud.flavor_info[:ebs_optimizable] + # description[:ebs_optimized] = cloud.ebs_optimized + #end + description + end + + # An array of hashes with dorky-looking keys, just like Fog wants it. + def self.block_device_mapping(computer) + Ironfan.todo "CODE SMELL: Machine is too familiar with EbsVolume problems" + computer.drives.values.map do |drive| + next if drive.disk # Don't create any disc already satisfied + volume = drive.volume or next + hsh = { 'DeviceName' => volume.device } + if volume.attachable == 'ephemeral' + hsh['VirtualName'] = drive.name + # if set for creation at launch (and not already created) + elsif drive.node[:volume_id].blank? && volume.create_at_launch + if volume.snapshot_id.blank? && volume.size.blank? + raise "Must specify a size or a snapshot ID for #{volume}" + end + hsh['Ebs.SnapshotId'] = volume.snapshot_id if volume.snapshot_id.present? + hsh['Ebs.VolumeSize'] = volume.size.to_s if volume.size.present? + hsh['Ebs.DeleteOnTermination'] = (not volume.keep).to_s + else next + end + hsh + end.compact + end + + def self.destroy!(computer) + return unless computer.machine? + forget computer.machine.name + computer.machine.destroy + computer.machine.reload # show the node as shutting down + end + + def self.save!(computer) + return unless computer.machine? + # the EC2 API does not surface disable_api_termination as a value, so we + # have to set it every time. + permanent = computer.server.cloud(:openstack).permanent + return unless computer.created? + #Ironfan.step(computer.name, "setting termination flag #{permanent}", :blue) + #Ironfan.unless_dry_run do + # Ironfan.safely do + # OpenStack.connection.modify_instance_attribute( computer.machine.id, + # {'DisableApiTermination.Value' => computer.permanent?, }) + # end + #end + end + end + + end + end +end diff --git a/lib/ironfan/provider/openstack/security_group.rb b/lib/ironfan/provider/openstack/security_group.rb new file mode 100644 index 00000000..27035eca --- /dev/null +++ b/lib/ironfan/provider/openstack/security_group.rb @@ -0,0 +1,224 @@ +module Ironfan + class Provider + class OpenStack + + class SecurityGroup < Ironfan::Provider::Resource + + WIDE_OPEN = Range.new(1,65535) + + delegate :_dump, :authorize_group_and_owner, :authorize_port_range, + :collection, :collection=, :connection, :connection=, :description, + :description=, :destroy, :group_id, :group_id=, :identity, + :identity=, :ip_permissions=, :name, :name=, + :new_record?, :owner_id, :owner_id=, :reload, :requires, + :requires_one, :revoke_group_and_owner, :revoke_port_range, :save, + :symbolize_keys, :wait_for, :name, + :create_security_group_rule, + :rules, + :to => :adaptee + + def self.shared?() true; end + def self.multiple?() true; end + def self.resource_type() :security_group; end + + def self.expected_ids(computer) + return unless computer.server + openstack = computer.server.cloud(:openstack) + server_groups = computer.server.security_groups + cloud_groups = openstack.security_groups + + result = [] + [server_groups, cloud_groups].each do |container| + container.each { |g| result.push(g.name) } + end + return result.uniq + end + + def ip_permissions + end + + def group_id + @adaptee.id + end + + # + # Discovery + # + def self.load!(cluster=nil) + OpenStack.connection.security_groups.reject { |raw| raw.blank? }.each do |raw| + remember SecurityGroup.new(:adaptee => raw) + end + end + + def receive_adaptee(obj) + obj = OpenStack.connection.security_groups.new(obj) if obj.is_a?(Hash) + super + end + + def to_s + if ip_permissions.present? + perm_str = ip_permissions.map{|perm| + "%s:%s-%s (%s | %s)" % [ + perm['ipProtocol'], perm['fromPort'], perm['toPort'], + perm['groups' ].map{|el| el['groupName'] }.join(','), + perm['ipRanges'].map{|el| el['cidrIp'] }.join(','), + ] + } + return "<%-15s %-12s %-25s %s>" % [ self.class.handle, group_id, name, perm_str] + else + return "<%-15s %-12s %s>" % [ self.class.handle, group_id, name ] + end + end + + # + # Manipulation + # + def self.prepare!(computers) + # Create any groups that don't yet exist, and ensure any authorizations + # that are required for those groups + cluster_name = nil + groups_to_create = [ ] + authorizations_to_ensure = [ ] + + computers.each{|comp| ensure_groups(comp) if OpenStack.applicable(comp) } # Add facet and cluster security groups for the computer + + # First, deduce the list of all groups to which at least one instance belongs + # We'll use this later to decide whether to create groups, or authorize access, + # using a VPC security group or an Openstack security group. + groups_that_should_exist = [] #computers.map{|comp| expected_ids(comp) }.flatten.compact.sort.uniq + groups_to_create << groups_that_should_exist + + computers.select { |computer| OpenStack.applicable computer }.each do |computer| + cloud = computer.server.cloud(:openstack) + cluster_name = computer.server.cluster_name + + # Iterate over all of the security group information, keeping track of + # any groups that must exist and any authorizations that must be ensured + [computer.server.security_groups, cloud.security_groups].each do |container| + + container.values.each do |dsl_group| + + groups_to_create << dsl_group.name + + groups_to_create << dsl_group.group_authorized + + groups_to_create << dsl_group.group_authorized_by + + authorizations_to_ensure << dsl_group.group_authorized.map do |other_group| + { + :grantor => dsl_group.name, + :grantee => other_group, + :grantee_type => :group, + :range => WIDE_OPEN, + } + end + + authorizations_to_ensure << dsl_group.group_authorized_by.map do |other_group| + { + :grantor => other_group, + :grantee => dsl_group.name, + :grantee_type => :group, + :range => WIDE_OPEN, + } + end + + authorizations_to_ensure << dsl_group.range_authorizations.map do |range_auth| + range, cidr, protocol = range_auth + { + :grantor => dsl_group.name, + :grantee => { :cidr_ip => cidr, :ip_protocol => protocol }, + :grantee_type => :cidr, + :range => range, + } + end + end + end + end + groups_to_create = groups_to_create.flatten.uniq.reject { |group| recall? group.to_s }.sort + authorizations_to_ensure = authorizations_to_ensure.flatten.uniq.sort { |a,b| a[:grantor] <=> b[:grantor] } + + Ironfan.step(cluster_name, "creating security groups", :blue) unless groups_to_create.empty? + groups_to_create.each do |group| + if group =~ /\// + Ironfan.step(group, " assuming that owner/group pair #{group} already exists", :blue) + else + Ironfan.step(group, " creating #{group} security group", :blue) + begin + tokens = group.to_s.split(':') + group_id = tokens.pop + OpenStack.connection.create_security_group(group_id,"Ironfan created group #{group_id}") + rescue Fog::Compute::OpenStack::Error => e # InvalidPermission.Duplicate + Chef::Log.info("ignoring security group error: #{e}") + end + end + end + + # Re-load everything so that we have a @@known list of security groups to manipulate + load! unless groups_to_create.empty? + + # Now make sure that all required authorizations are present + Ironfan.step(cluster_name, "ensuring security group permissions", :blue) unless authorizations_to_ensure.empty? + authorizations_to_ensure.each do |auth| + grantor_fog = recall(auth[:grantor]) + if :group == auth[:grantee_type] + if fog_grantee = recall(auth[:grantee]) + options = { :group => fog_grantee.group_id } + elsif auth[:grantee] =~ /\// + options = { :group_alias => auth[:grantee] } + else + raise "Don't know what to do with authorization grantee #{auth[:grantee]}" + end + message = " ensuring access from #{auth[:grantee]} to #{auth[:grantor]}" + else + options = auth[:grantee] + message = " ensuring #{auth[:grantee][:ip_protocol]} access from #{auth[:grantee][:cidr_ip]} to #{auth[:range]}" + end + Ironfan.step(auth[:grantor], message, :blue) + safely_authorize(grantor_fog, auth[:range], options) + end + end + + # + # Utility + # + def self.ensure_groups(computer) + return unless OpenStack.applicable computer + # Ensure the security_groups include those for cluster & facet + # FIXME: This violates the DSL's immutability; it should be + # something calculated from within the DSL construction + Ironfan.todo("CODE SMELL: violation of DSL immutability: #{caller}") + server = computer.server + c_group = server.security_group(computer.server.cluster_name) + c_group.authorized_by_group(c_group.name) + facet_name = "#{computer.server.cluster_name}-#{computer.server.facet_name}" + server.security_group(facet_name) + end + + + # Try an authorization, ignoring duplicates (this is easier than correlating). + # Do so for both TCP and UDP, unless only one is specified + def self.safely_authorize(fog_group,range,options) + if options[:group_alias] + owner, group = options[:group_alias].split(/\//) + self.patiently(fog_group.name, Fog::Compute::OpenStack::Error, :ignore => Proc.new { |e| e.message =~ /This rule already exists in group/ }) do + OpenStack.connection.authorize_security_group_ingress( + 'GroupName' => fog_group.name, + 'SourceSecurityGroupName' => group, + 'SourceSecurityGroupOwnerId' => owner + ) + end + elsif options[:ip_protocol] + self.patiently(fog_group.name, Excon::Errors::HTTPStatusError, :ignore => Proc.new { |e| e.message =~ /This rule already exists in group/ }) do + fog_group.create_security_group_rule(range.min, range.max, options[:ip_protocol], options[:cidr_ip], options[:group]) + end + else + safely_authorize(fog_group,range,options.merge(:ip_protocol => 'tcp')) + safely_authorize(fog_group,range,options.merge(:ip_protocol => 'udp')) + safely_authorize(fog_group,Range.new(-1,-1),options.merge(:ip_protocol => 'icmp')) if(range == WIDE_OPEN) + return + end + end + end + end + end +end diff --git a/lib/ironfan/provider/static.rb b/lib/ironfan/provider/static.rb new file mode 100644 index 00000000..5236a10f --- /dev/null +++ b/lib/ironfan/provider/static.rb @@ -0,0 +1,23 @@ +module Ironfan + class Provider + class Static < Ironfan::IaasProvider + self.handle = :static + + def self.resources + [ Machine ] + end + + def self.ensure_tags(tags,fog) + # Ironfan.safely do + # config = {:key => k, :value => v.to_s, :resource_id => fog.id } + # connection.tags.create(config) + # end + #end + end + + def self.applicable(computer) + computer.server and computer.server.clouds.include?(:static) + end + end + end +end diff --git a/lib/ironfan/provider/static/machine.rb b/lib/ironfan/provider/static/machine.rb new file mode 100644 index 00000000..296d6db0 --- /dev/null +++ b/lib/ironfan/provider/static/machine.rb @@ -0,0 +1,192 @@ +module Ironfan + class Provider + class Static + class Machine < Ironfan::IaasProvider::Machine + + def self.shared?() false; end + def self.multiple?() false; end + def self.resource_type() :machine; end + def self.expected_ids(computer) [computer.server.full_name]; end + + def name() adaptee.full_name; end + def tags + t = {"Name" => @adaptee.name} + return t.keys.inject({}) {|h,k| h[k]=t[k]; h[k.to_sym]=t[k]; h} + end + + def vpc_id + return nil + end + + def created_at + nil + end + + def flavor_id + return nil + end + + def flavor_name + "nil" + end + + def image_id + "none" + end + + def groups ; [] ; end + + def public_hostname ; adaptee.cloud(:static).public_hostname || public_ip_address ; end + def public_ip_address ; adaptee.cloud(:static).public_ip || private_ip_address ; end + def dns_name ; public_ip_address ; end + + def keypair ; adaptee.cloud(:static).keypair ; end + + def created? + false + end + def pending? + false + end + def running? + true + end + def stopping? + false + end + + def stopped? + false + end + + def error? + false + end + + def start + end + + def stop + end + + def perform_after_launch_tasks? + true + end + + def to_display(style,values={}) + # style == :minimal + values["State"] = "???" + values["MachineID"] = private_ip_address + values["Public IP"] = private_ip_address + values["Private IP"] = public_ip_address + values["Created On"] = "???" + return values if style == :minimal + + # style == :default + values["Flavor"] = nil + values["AZ"] = nil + return values if style == :default + + # style == :expanded + values["Image"] = "none" + #values["Volumes"] = volumes.map(&:id).join(', ') + values["SSH Key"] = "none" + values + end + + def ssh_key + keypair = cloud.keypair || computer.server.cluster_name + end + + def key_name + keypair + end + + def private_ip_address + adaptee.cloud(:static).private_ip + end + + def availability_zone + 'none' + end + + def destroy + end + + def to_s + "<%-15s %-12s %-25s %-25s %-15s %-15s %-12s %-12s %s:%s>" % [ + self.class.handle, "", created_at, name, private_ip_address, public_ip_address, flavor_name, availability_zone, key_name, groups.join(',') ] + end + + # + # Discovery + # + def self.load!(cluster=nil) + cluster.facets.each do |facet| + facet.servers.each do |server| + machine = new(:adaptee => server) + remember machine + end + end + end + + # Find active machines that haven't matched, but should have, + # make sure all bogus machines have a computer to attach to + # for display purposes + def self.validate_resources!(computers) + recall.each_value do |machine| + next unless machine.users.empty? and machine.name + if machine.name.match("^#{computers.cluster.name}-") + machine.bogus << :unexpected_machine + end + next unless machine.bogus? + fake = Ironfan::Broker::Computer.new + fake[:machine] = machine + fake.name = machine.name + machine.users << fake + computers << fake + end + end + + # + # Manipulation + # + def self.create!(computer) + nil + end + + # @returns [Hash{String, Array}] of 'what you did wrong' => [relevant, info] + def self.lint(computer) + cloud = computer.server.cloud(:static) + info = [computer.name, cloud.inspect] + errors = {} + server_errors = computer.server.lint + errors["No Private IP"] = info if cloud.private_ip.blank? + errors + end + + def self.launch_description(computer) + nil + end + + # An array of hashes with dorky-looking keys, just like Fog wants it. + def self.block_device_mapping(computer) + [] + end + + def self.destroy!(computer) + return unless computer.machine? + forget computer.machine.name + computer.machine.destroy + computer.machine.reload # show the node as shutting down + end + + def self.save!(computer) + return unless computer.machine? + return unless computer.created? + nil + end + end + end + end +end diff --git a/lib/ironfan/requirements.rb b/lib/ironfan/requirements.rb index e58ccdfd..b235e82d 100644 --- a/lib/ironfan/requirements.rb +++ b/lib/ironfan/requirements.rb @@ -15,6 +15,7 @@ require 'ironfan/builder' require 'ironfan/dsl/component' +require 'ironfan/dsl/security_group' require 'ironfan/dsl/compute' require 'ironfan/dsl/server' require 'ironfan/dsl/facet' @@ -26,6 +27,8 @@ require 'ironfan/dsl/cloud' require 'ironfan/dsl/ec2' +require 'ironfan/dsl/openstack' +require 'ironfan/dsl/static' require 'ironfan/dsl/vsphere' require 'ironfan/dsl/rds' @@ -48,6 +51,15 @@ require 'ironfan/provider/ec2/elastic_load_balancer' require 'ironfan/provider/ec2/iam_server_certificate' +require 'ironfan/provider/openstack' +require 'ironfan/provider/openstack/machine' +require 'ironfan/provider/openstack/keypair' +require 'ironfan/provider/openstack/security_group' +require 'ironfan/provider/openstack/elastic_ip' + +require 'ironfan/provider/static' +require 'ironfan/provider/static/machine' + require 'ironfan/provider/virtualbox' require 'ironfan/provider/virtualbox/machine'