<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -39,13 +39,13 @@ dns_providers:
   nettica:
     user: joe
     password: sekret
-    record_type: A
+    type: A
     ttl: 300
   zerigo:
     customer_id: 1234
     email: foo@bar.com
     token: hexxy
-    record_type: A
+    type: A
     ttl: 300
   dyndns:
     user: joe
@@ -55,6 +55,61 @@ dns_providers:
 # OPTIONAL: The dns provider to use
 # dns_provider: nettica
 
+# OPTIONAL: Lets you configure your dns service, for example to add other CNAMES
+#           or setup dns round robin, etc.
+#
+#  dns_records:
+#    # simple A record
+#    - host: bar
+#      data: 1.1.1.1
+#
+#    # more detailed A record
+#    - host: bar
+#      domain: otherdomain.com
+#      data: 1.1.1.1
+#      type: A
+#      ttl: 300
+#
+#    # tld A record
+#    - host: ''
+#      data: 1.1.1.1
+#      type: A
+#
+#    # simple CNAME record
+#    - host: otherbar
+#      domain: foo.com
+#      data: bar.foo.com
+#      type: CNAME
+#      ttl: 300
+#
+#    # 2 of the same A records is a round robin dns
+#    - host: rr
+#      domain: foo.com
+#      data: 1.1.1.1
+#      type: A
+#      ttl: 300
+#    - host: rr
+#      domain: foo.com
+#      data: 1.1.1.2
+#      type: A
+#      ttl: 300
+#
+#    # A record, grabbing ip from instance config
+#    - host: baz
+#      domain: foo.com
+#      data: &quot;#{rubber_instances.for_role('web').first.external_ip}&quot;
+#      type: A
+#      ttl: 300
+#
+#    # MX record
+#    - host: mail
+#      domain: foo.com
+#      data: 1.1.1.1
+#      type: MX
+#      ttl: 300
+#      priority: 10
+    
+
 # OPTIONAL: Additional rubber file to pull config from if it exists.  This file will
 # also be pushed to remote host at RUBBER_ROOT/config/rubber/rubber-secret.yml
 #</diff>
      <filename>generators/vulcanize/templates/base/config/rubber/rubber.yml</filename>
    </modified>
    <modified>
      <diff>@@ -3,65 +3,68 @@ module Rubber
 
     class Base
 
-      attr_reader :env
+      attr_reader :env, :provider_env, :provider_name
 
-      def initialize(env)
+      def initialize(env, provider_name)
         @env = env
+        @provider_name = provider_name
+        @provider_env = @env.dns_providers[provider_name]
       end
 
       def update(host, ip)
         if up_to_date(host, ip)
           puts &quot;IP has not changed for #{host}, not updating dynamic DNS&quot;
         else
-          if ! host_exists?(host)
+          if find_host_records(:host =&gt; host).size == 0
             puts &quot;Creating dynamic DNS: #{host} =&gt; #{ip}&quot;
-            create_host_record(host, ip)
+            create_host_record(:host =&gt; host, :data =&gt; ip)
           else
             puts &quot;Updating dynamic DNS: #{host} =&gt; #{ip}&quot;
-            update_host_record(host, ip)
+            update_host_record({:host =&gt; host}, {:host =&gt; host, :data =&gt; ip})
           end
         end
       end
 
       def destroy(host)
-        if host_exists?(host)
+        if find_host_records(:host =&gt; host).size != 0
           puts &quot;Destroying dynamic DNS record: #{host}&quot;
-          destroy_host_record(host)
+          destroy_host_record(:host =&gt; host)
         end
       end
 
-      def hostname(host)
-        &quot;#{host}.#{@env.domain}&quot;
-      end
-
       def up_to_date(host, ip)
-        # This queries dns server directly instead of using hosts file
-        current_ip = nil
-        Resolv::DNS.open(:nameserver =&gt; [nameserver], :search =&gt; [], :ndots =&gt; 1) do |dns|
-          current_ip = dns.getaddress(hostname(host)).to_s rescue nil
-        end
-        return ip == current_ip
+        find_host_records(:host =&gt; host).any? {|host| host[:data] == ip}
       end
 
-      def nameserver()
-        raise &quot;nameserver not implemented&quot;
+      def create_host_record(opts = {})
+        raise &quot;create_host_record not implemented&quot;
       end
 
-      def host_exists?(host)
-        raise &quot;host_exists? not implemented&quot;
+      def find_host_records(opts = {})
+        raise &quot;find_host_records not implemented&quot;
       end
 
-      def create_host_record(host, ip)
-        raise &quot;create_host_record not implemented&quot;
+      def update_host_record(old_opts={}, new_opts={})
+        raise &quot;update_host_record not implemented&quot;
       end
 
-      def destroy_host_record(host)
+      def destroy_host_record(opts = {})
         raise &quot;destroy_host_record not implemented&quot;
       end
 
-      def update_host_record(host, ip)
-        raise &quot;update_host_record not implemented&quot;
+      protected
+
+      def setup_opts(opts, required =[])
+        default_opts = {:domain =&gt; @provider_env.domain,
+                        :type =&gt; @provider_env['type'] || @provider_env.record_type,
+                        :ttl =&gt; @provider_env.ttl}
+        actual_opts = default_opts.merge(Rubber::Util::symbolize_keys(opts))
+        required.each do |r|
+          raise &quot;Missing required options: #{r}&quot; unless actual_opts[r]
+        end
+        return actual_opts
       end
+      
 
     end
 </diff>
      <filename>lib/rubber/dns/base.rb</filename>
    </modified>
    <modified>
      <diff>@@ -4,10 +4,9 @@ module Rubber
     class Dyndns &lt; Base
 
       def initialize(env)
-        super(env)
-        @dyndns_env = env.dns_providers.dyndns
-        @user, @pass = @dyndns_env.user, @dyndns_env.password
-        @update_url = @dyndns_env.update_url || 'https://members.dyndns.org/nic/update?hostname=%host%&amp;myip=%ip%'
+        super(env, 'dyndns')
+        @user, @pass = provider_env.user, provider_env.password
+        @update_url = provider_env.update_url || 'https://members.dyndns.org/nic/update?hostname=%host%&amp;myip=%ip%'
         @update_url = @update_url.gsub(/%([^%]+)%/, '#{\1}')
       end
 
@@ -15,10 +14,22 @@ module Rubber
         &quot;ns1.mydyndns.org&quot;
       end
 
-      def host_exists?(host)
+      def up_to_date(host, ip)
+        # This queries dns server directly instead of using hosts file
+        current_ip = nil
+        Resolv::DNS.open(:nameserver =&gt; [nameserver], :search =&gt; [], :ndots =&gt; 1) do |dns|
+          current_ip = dns.getaddress(&quot;#{host}.#{provider_env.domain}&quot;).to_s rescue nil
+        end
+        return ip == current_ip
+      end
+      
+      def find_host_records(opts={})
+        opts = setup_opts(opts, [:host, :domain])
+        hostname = &quot;#{opts[:host]}.#{opts[:domain]}&quot;
         begin
           Resolv::DNS.open(:nameserver =&gt; [nameserver], :search =&gt; [], :ndots =&gt; 1) do |dns|
-            dns.getresource(hostname(host), Resolv::DNS::Resource::IN::A)
+            r = dns.getresource(hostname, Resolv::DNS::Resource::IN::A)
+            result = [{:host =&gt;host, :data =&gt; r.address}]
           end
         rescue
           raise &quot;Domain needs to exist in dyndns as an A record before record can be updated&quot;
@@ -26,16 +37,20 @@ module Rubber
         return true
       end
 
-      def create_host_record(host, ip)
+      def create_host_record(opts={})
         puts &quot;WARNING: No create record available for dyndns, you need to do so manually&quot;
       end
 
-      def destroy_host_record(host)
+      def destroy_host_record(opts={})
         puts &quot;WARNING: No destroy record available for dyndns, you need to do so manually&quot;
       end
 
-      def update_host_record(host, ip)
-        host = hostname(host)
+      def update_host_record(old_opts={}, new_opts={})
+        old_opts = setup_opts(opts, [:host, :domain])
+        new_opts = setup_opts(opts, [:data])
+        
+        host = hostname(old_opts[:host])
+        ip = new_opts[:data]
         update_url = eval('%Q{' + @update_url + '}')
 
         # This header is required by dyndns.org</diff>
      <filename>lib/rubber/dns/dyndns.rb</filename>
    </modified>
    <modified>
      <diff>@@ -5,15 +5,8 @@ module Rubber
     class Nettica &lt; Base
 
       def initialize(env)
-        super(env)
-        @nettica_env = @env.dns_providers.nettica
-        @client = ::Nettica::Client.new(@nettica_env.user, @nettica_env.password)
-        @ttl = (@nettica_env.ttl || 300).to_i
-        @record_type = @nettica_env.record_type || &quot;A&quot;
-      end
-
-      def nameserver
-        &quot;dns1.nettica.com&quot;
+        super(env, &quot;nettica&quot;)
+        @client = ::Nettica::Client.new(provider_env.user, provider_env.password)
       end
 
       def check_status(response)
@@ -33,40 +26,78 @@ module Rubber
         return response
       end
 
-      def host_exists?(host)
-        domain_info = check_status @client.list_domain(env.domain)
+      def find_host_records(opts = {})
+        opts = setup_opts(opts, [:host, :domain])
+
+        result = []
+        hn = opts[:host]
+        ht = opts[:type]
+        hd = opts[:data]
+
+        domain_info = check_status @client.list_domain(opts[:domain])
         raise &quot;Domain needs to exist in nettica before records can be updated&quot; unless domain_info.record
-        return domain_info.record.any? { |r| r.hostName == host }
-      end
 
-      def create_host_record(host, ip)
-        new = @client.create_domain_record(env.domain, host, @record_type, ip, @ttl, 0)
-        check_status @client.add_record(new)
+        domain_info.record.each do |h|
+          keep = true
+          if hn &amp;&amp; h.hostName != hn &amp;&amp; hn != '*'
+            keep = false
+          end
+          if ht &amp;&amp; h.recordType != ht &amp;&amp; ht != '*'
+            keep = false
+          end
+          if hd &amp;&amp; h.data != hd
+            keep = false
+          end
+          result &lt;&lt; record_to_opts(h) if keep
+        end
+
+        return result
       end
 
-      def destroy_host_record(host)
-        old_record = check_status(@client.list_domain(env.domain)).record.find {|r| r.hostName == host }
-        old = @client.create_domain_record(env.domain, host, old_record.recordType, old_record.data, old_record.tTL, old_record.priority)
-        check_status @client.delete_record(old)
+      def create_host_record(opts = {})
+        opts = setup_opts(opts, [:host, :data, :domain, :type, :ttl])
+        record = opts_to_record(opts)
+        check_status @client.add_record(record)
       end
 
-      def update_host_record(host, ip)
-        old_record = check_status(@client.list_domain(env.domain)).record.find {|r| r.hostName == host }
-        update_record(host, ip, old_record)
+      def destroy_host_record(opts = {})
+        find_host_records(opts).each do |h|
+          record = opts_to_record(h)
+          check_status @client.delete_record(record)
+        end
       end
 
-      # update the top level domain record which has an empty hostName
-      def update_domain_record(ip)
-        old_record = check_status(@client.list_domain(env.domain)).record.find {|r| r.hostName == '' and r.recordType == 'A' and r.data =~ /\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/}
-        update_record('', ip, old_record)
+      def update_host_record(old_opts = {}, new_opts = {})
+        old_opts = setup_opts(old_opts, [:host, :domain])
+        find_host_records(old_opts).each do |h|
+          old_record = opts_to_record(h)
+          new_record = opts_to_record(h.merge(new_opts))
+          check_status @client.update_record(old_record, new_record)
+        end
       end
 
-      def update_record(host, ip, old_record)
-        old = @client.create_domain_record(env.domain, host, old_record.recordType, old_record.data, old_record.tTL, old_record.priority)
-        new = @client.create_domain_record(env.domain, host, @record_type, ip, @ttl, 0)
-        check_status @client.update_record(old, new)
+      private
+
+      def opts_to_record(opts)
+        record = @client.create_domain_record(opts[:domain],
+                                              opts[:host],
+                                              opts[:type],
+                                              opts[:data],
+                                              opts[:ttl],
+                                              opts[:priority] || 0)
+        return record
       end
 
+      def record_to_opts(record)
+        opts = {}
+        opts[:host] = record.hostName
+        opts[:domain] = record.domainName
+        opts[:type] = record.recordType
+        opts[:data] = record.data
+        opts[:ttl] = record.tTL
+        opts[:priority] = record.priority
+        return opts
+      end
     end
 
   end</diff>
      <filename>lib/rubber/dns/nettica.rb</filename>
    </modified>
    <modified>
      <diff>@@ -9,6 +9,11 @@ module Rubber
       include HTTParty
       format :xml
 
+      @@zones = {}
+      def self.get_zone(domain, provider_env)
+       @@zones[domain] ||= Zone.new(provider_env.customer_id, provider_env.email, provider_env.token, domain)
+      end
+
       def initialize(customer_id, email, token, domain)
         self.class.basic_auth email, token
         self.class.base_uri &quot;https://ns.zerigo.com/accounts/#{customer_id}&quot;
@@ -26,32 +31,49 @@ module Rubber
         return response
       end
 
-      def hosts
-        hosts = check_status self.class.get(&quot;/zones/#{@zone['id']}/hosts.xml&quot;)
-        return hosts['hosts']
+      def create_host(opts)
+        host = opts_to_host(opts, new_host())
+        check_status self.class.post(&quot;/zones/#{@zone['id']}/hosts.xml&quot;, :body =&gt; {:host =&gt; host})
       end
 
-      def host(hostname)
+      def find_host_records(opts={})
+        result = []
+        hn = opts[:host]
+        ht = opts[:type]
+        hd = opts[:data]
+        has_host = hn &amp;&amp; hn != '*'
+
+        url = &quot;/zones/#{@zone['id']}/hosts.xml&quot;
+        if has_host
+          url &lt;&lt; &quot;?fqdn=&quot;
+          url &lt;&lt; &quot;#{hn}.&quot; if hn.strip.size &gt; 0
+          url &lt;&lt; &quot;#{@domain}&quot;
+        end
+        hosts = self.class.get(url)
+
         # returns 404 on not found, so don't check status
-        hosts = self.class.get(&quot;/zones/#{@zone['id']}/hosts.xml?fqdn=#{hostname}.#{@domain}&quot;)
-        return (hosts['hosts'] || []).first
-      end
+        hosts = check_status hosts unless has_host
 
-      def new_host
-        check_status(self.class.get(&quot;/zones/#{@zone['id']}/hosts/new.xml&quot;))['host']
-      end
+        hosts['hosts'].each do |h|
+          keep = true
+          if ht &amp;&amp; h['host_type'] != ht &amp;&amp; ht != '*'
+            keep = false
+          end
+          if hd &amp;&amp; h['data'] != hd
+            keep = false
+          end
+          result &lt;&lt; host_to_opts(h) if keep
+        end if hosts['hosts']
 
-      def create_host(host)
-        check_status self.class.post(&quot;/zones/#{@zone['id']}/hosts.xml&quot;, :body =&gt; {:host =&gt; host})
+        return result
       end
 
-      def update_host(host)
-        host_id = host['id']
+      def update_host(host_id, opts)
+        host = opts_to_host(opts, new_host())
         check_status self.class.put(&quot;/zones/#{@zone['id']}/hosts/#{host_id}.xml&quot;, :body =&gt; {:host =&gt; host})
       end
 
-      def delete_host(hostname)
-        host_id = host(hostname)['id']
+      def delete_host(host_id)
         check_status self.class.delete(&quot;/zones/#{@zone['id']}/hosts/#{host_id}.xml&quot;)
       end
 
@@ -63,67 +85,84 @@ module Rubber
           zones = check_status self.class.get('/zones.xml')
           @zone = zones[&quot;zones&quot;].find {|z| z[&quot;domain&quot;] == @domain }
         end
+        if ! @zone
+          zone = new_zone()
+          zone['domain'] = @domain
+          @zone = check_status self.class.post('/zones.xml', :body =&gt; {:zone =&gt; zone})
+        end
       end
 
-      def data
+      def zone_record
         return @zone
       end
 
-      protected
+      private
 
-      def zones()
-        check_status self.class.get('/zones.xml')
+      def new_host
+        check_status(self.class.get(&quot;/zones/#{@zone['id']}/hosts/new.xml&quot;))['host']
+      end
+
+      def new_zone
+        check_status(self.class.get(&quot;/zones/new.xml&quot;))['zone']
+      end
+
+      def opts_to_host(opts, host={})
+        host['hostname'] = opts[:host]
+        host['host_type'] =  opts[:type]
+        host['data'] = opts[:data]
+        host['ttl'] = opts[:ttl]
+        host['priority'] = opts[:priority]
+        return host
       end
 
-      def zone(domain_name)
-        zone =  zones
-        return zone
+      def host_to_opts(host)
+        opts = {}
+        opts[:id] = host['id'] 
+        opts[:host] = host['hostname']
+        opts[:type] = host['host_type']
+        opts[:data] = host['data']
+        opts[:ttl] = host['ttl']
+        opts[:priority] = host['priority']
+        return opts
       end
-      
     end
     
     class Zerigo &lt; Base
 
       def initialize(env)
-        super(env)
-        @zerigo_env = env.dns_providers.zerigo
-        @ttl = (@zerigo_env.ttl || 300).to_i
-        @record_type = @zerigo_env.record_type || &quot;A&quot;
-        @zone = Zone.new(@zerigo_env.customer_id, @zerigo_env.email, @zerigo_env.token, env.domain)
+        super(env, &quot;zerigo&quot;)
       end
 
-      def nameserver
-        &quot;a.ns.zerigo.net&quot;
-      end
+      def find_host_records(opts = {})
+        opts = setup_opts(opts, [:host, :domain])
+        zone = Zone.get_zone(opts[:domain], provider_env)
 
-      def host_exists?(host)
-        @zone.host(host)
+        zone.find_host_records(opts)
       end
 
-      def create_host_record(hostname, ip)
-        host = @zone.new_host()
-        host['host-type'] =  @record_type
-        host['ttl'] = @ttl
-        host['hostname'] = hostname
-        host['data'] = ip
-        @zone.create_host(host)
-      end
+      def create_host_record(opts = {})
+        opts = setup_opts(opts, [:host, :data, :domain, :type, :ttl])
+        zone = Zone.get_zone(opts[:domain], provider_env)
 
-      def destroy_host_record(host)
-        @zone.delete_host(host)
+        zone.create_host(opts)
       end
 
-      def update_host_record(host, ip)
-        old = @zone.host(host)
-        old['data'] = ip
-        @zone.update_host(old)
+      def destroy_host_record(opts = {})
+        opts = setup_opts(opts, [:host, :domain])
+        zone = Zone.get_zone(opts[:domain], provider_env)
+
+        find_host_records(opts).each do |h|
+          zone.delete_host(h[:id])
+        end
       end
 
-      # update the top level domain record which has an empty hostName
-      def update_domain_record(ip)
-        old = @zone.hosts.find {|h| h['hostname'].nil? }
-        old['data'] = ip
-        @zone.update_host(old)
+      def update_host_record(old_opts={}, new_opts={})
+        old_opts = setup_opts(old_opts, [:host, :domain])
+        zone = Zone.get_zone(old_opts[:domain], provider_env)
+
+        find_host_records(old_opts).each do |h|
+          zone.update_host(h[:id], h.merge(new_opts))
+        end
       end
 
     end</diff>
      <filename>lib/rubber/dns/zerigo.rb</filename>
    </modified>
    <modified>
      <diff>@@ -32,6 +32,8 @@ module Rubber
         roles_dir = File.join(@config_root, &quot;role&quot;)
         roles = Dir.entries(roles_dir)
         roles.delete_if {|d| d =~ /(^\..*)/}
+        roles += @items['roles'].keys
+        return roles.compact.uniq
       end
 
       def current_host
@@ -41,7 +43,7 @@ module Rubber
       def current_full_host
         Socket::gethostname
       end
-
+      
       def bind(roles = nil, host = nil)
         BoundEnv.new(@items, roles, host)
       end
@@ -95,6 +97,7 @@ module Rubber
         end
 
         def expand_string(val)
+          rubber_instances = Rubber::Configuration::rubber_instances
           while val =~ /\#\{[^\}]+\}/
             val = eval('%Q{' + val + '}', binding)
           end</diff>
      <filename>lib/rubber/environment.rb</filename>
    </modified>
    <modified>
      <diff>@@ -115,4 +115,19 @@ class EnvironmentTest &lt; Test::Unit::TestCase
     assert_equal 'val5', e.var2.var9, 'env not retrieving right val'
   end
 
+  def test_instances_in_expansion
+    instance = InstanceItem.new('host1', 'domain.com', [RoleItem.new('role1')], '')
+    instance.external_ip = &quot;1.2.3.4&quot;
+    instances = Instance.new(Tempfile.new('testforinstanceexpansion').path)
+    instances.add(instance)
+
+    File.expects(:exist?).returns(true)
+    YAML.expects(:load_file).returns({'var1' =&gt;'&quot;#{rubber_instances.for_role(&quot;role1&quot;).first.external_ip}&quot;'})
+    Rubber::Configuration.expects(:rubber_instances).returns(instances)
+    env = Rubber::Configuration::Environment.new(nil)
+    e = env.bind()
+
+    assert_equal &quot;\&quot;1.2.3.4\&quot;&quot;, e['var1']    
+  end
+
 end</diff>
      <filename>test/environment_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -2,3 +2,7 @@ $:.unshift &quot;#{File.dirname(__FILE__)}/../lib&quot;
 
 require 'rubber'
 Rubber::initialize(File.dirname(__FILE__), 'test')
+
+require 'rubygems'
+require 'mocha'
+require 'pp'</diff>
      <filename>test/test_helper.rb</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>c9c3e52a4f36cc19623c960c28b9917920e955d6</id>
    </parent>
  </parents>
  <author>
    <name>Matt Conway</name>
    <email>wr0ngway@yahoo.com</email>
  </author>
  <url>http://github.com/wr0ngway/rubber/commit/937629cad2c4ce7cb3601724a0bebd09de8e7008</url>
  <id>937629cad2c4ce7cb3601724a0bebd09de8e7008</id>
  <committed-date>2009-10-05T13:51:05-07:00</committed-date>
  <authored-date>2009-10-05T13:51:05-07:00</authored-date>
  <message>refactor dns classes to allow setting up other dns records from rubber.yml</message>
  <tree>2bf5afa6fc18167b47732f41817a092eff78f0c2</tree>
  <committer>
    <name>Matt Conway</name>
    <email>wr0ngway@yahoo.com</email>
  </committer>
</commit>
