arcus 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .idea
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.swp ADDED
Binary file
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in arcus.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 atistler
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1 @@
1
+ This file was created by JetBrains RubyMine 4.0.1 for binding GitHub repository
@@ -0,0 +1,53 @@
1
+ # Arcus
2
+
3
+ A library that provides a clean API into Cloudstack REST calls. Also included is a CLI tool for making Cloudstack REST calls
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'arcus'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install arcus
18
+
19
+ ## Usage
20
+
21
+ Cloudstack REST commands are read in from the commands.xml file. This provides a list of required and optional arguments
22
+ for each Cloudstack command. Each Cloudstack command is split up into an action and a target, for instance listVirtualMachines
23
+ becomes: [action -> list, target -> VirtualMachine]. The arcus will create a dynamic class based on the name of the target and
24
+ a method named after the action. So you can do the following:
25
+
26
+ vms = VirtualMachine.new.list.fetch
27
+
28
+ vms will be a object containing the results of "listvirtualmachinesresponse"
29
+
30
+ Other "response_types" are allowed, for example:
31
+
32
+ vms = VirtualMachine.new.list.fetch(:yaml)
33
+ vms = VirtualMachine.new.list.fetch(:xml)
34
+ vms = VirtualMachine.new.list.fetch(:prettyxml)
35
+ vms = VirtualMachine.new.list.fetch(:json)
36
+ vms = VirtualMachine.new.list.fetch(:prettyjson)
37
+
38
+ Each of these calls will return a string representation in the format specified.
39
+
40
+ You can also give arguments to the "action" method. For example:
41
+
42
+ vms = VirtualMachine.new.list({id: 1}).fetch(:json)
43
+
44
+ will produce the http call -> /client/api?id=1&response=xml&command=listVirtualMachines
45
+
46
+
47
+ ## Contributing
48
+
49
+ 1. Fork it
50
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
51
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
52
+ 4. Push to the branch (`git push origin my-new-feature`)
53
+ 5. Create new Pull Request
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/arcus/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["atistler"]
6
+ gem.email = ["atistler@gmail.com"]
7
+ gem.description = %q{API and client CLI tool for cloudstack}
8
+ gem.summary = %q{API and client CLI tool for cloudstack}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "arcus"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Arcus::VERSION
17
+
18
+ gem.add_dependency "active_support"
19
+ gem.add_dependency "nori"
20
+ gem.add_dependency "nokogiri"
21
+ gem.add_dependency "cmdparse"
22
+ end
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'arcus/cli'
4
+ require 'yaml'
5
+
6
+
7
+ rc_file = "#{Dir.home}/.arcusrc"
8
+ $settings = {}
9
+ if (File.exists? rc_file)
10
+ $settings = YAML.load_file(rc_file)
11
+ end
12
+
13
+ def process_env(setting, key)
14
+ if ENV.has_key?(key) && ! ENV[key].empty?
15
+ $settings[setting] = ENV[key]
16
+ end
17
+ end
18
+
19
+ process_env("api_uri", "ARCUS_API_URI")
20
+ process_env("default_response", "ARCUS_DEFAULT_RESPONSE")
21
+
22
+ error = if ! $settings.has_key? "api_uri"
23
+ "API URI must me set in either #{rc_file} or as a environmental variable (ARCUS_API_URI)"
24
+ elsif ! $settings.has_key? "default_response"
25
+ "Default response must me set in either #{rc_file} or as a environmental variable (ARCUS_DEFAULT_RESPONSE)"
26
+ end
27
+
28
+ if error
29
+ puts error
30
+ exit(false)
31
+ end
32
+
33
+ arcus = Arcus::Cli.new(
34
+ :api_uri => URI($settings["api_uri"]),
35
+ :default_response => $settings["default_response"]
36
+ )
37
+
38
+ arcus.run
@@ -0,0 +1,10 @@
1
+ require "arcus/version"
2
+ require "arcus/api"
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
@@ -0,0 +1,263 @@
1
+ require 'net/http'
2
+ require 'active_support/core_ext'
3
+ require 'active_support/inflector'
4
+ require 'nori'
5
+ require 'nokogiri'
6
+ require 'active_support/core_ext/hash/reverse_merge'
7
+ require 'arcus/helpers'
8
+ require 'logger'
9
+ require 'json'
10
+
11
+ module Arcus
12
+ Nori.parser = :nokogiri
13
+ Nori.advanced_typecasting = false
14
+
15
+ class << self
16
+ attr_accessor :log
17
+ end
18
+
19
+ module Api
20
+
21
+ @@targets = []
22
+ @@verbose = false
23
+ @@default_response = "json"
24
+
25
+ def self.verbose=(val)
26
+ @@verbose = val
27
+ end
28
+
29
+ def self.verbose
30
+ @@verbose
31
+ end
32
+
33
+ def self.default_response=(val)
34
+ @@default_response = val
35
+ end
36
+
37
+ def self.default_response
38
+ @@default_response
39
+ end
40
+
41
+ def self.targets
42
+ @@targets
43
+ end
44
+
45
+ class Request
46
+ attr_accessor :command_name, :params, :api_uri, :callbacks
47
+
48
+ def initialize(command_name, params, api_uri, callbacks)
49
+ @command_name = command_name
50
+ @params = params
51
+ @api_uri = api_uri
52
+ @callbacks = callbacks
53
+ end
54
+
55
+ def fetch(response_type = :object)
56
+ @params[:response] =
57
+ case response_type
58
+ when :json, :yaml, :prettyjson, :object
59
+ "json"
60
+ when :xml, :prettyxml
61
+ "xml"
62
+ end
63
+ http = Net::HTTP.new(@api_uri.host, @api_uri.port)
64
+ http.read_timeout = 5
65
+ http.open_timeout = 1
66
+ req_url = api_uri.path + "?" + URI.encode_www_form(params.merge({:command => @command_name}))
67
+ Arcus.log.debug { "Sending: #{req_url}" } if Api.verbose == true
68
+ response = begin
69
+ http.get(req_url)
70
+ rescue Timeout::Error => e
71
+ e.instance_eval do
72
+ class << self
73
+ attr_accessor :api_uri
74
+ end
75
+ end
76
+ e.api_uri = @api_uri
77
+ raise e
78
+ end
79
+ Arcus.log.debug { "Received: #{response.body}" } if Api.verbose == true
80
+ response.instance_eval do
81
+ class << self
82
+ attr_accessor :response_type
83
+ end
84
+
85
+ def body
86
+ case @response_type
87
+ when :yaml
88
+ YAML::dump(JSON.parse(super))
89
+ when :prettyjson
90
+ JSON.pretty_generate(JSON.parse(super))
91
+ when :prettyxml
92
+ Nokogiri::XML(super, &:noblanks)
93
+ when :xml, :json
94
+ super
95
+ when :object
96
+ JSON.parse(super)
97
+ end
98
+ end
99
+ end
100
+ response.response_type = response_type
101
+ if response.is_a?(Net::HTTPSuccess)
102
+ callbacks[:success].call(response) if callbacks[:success]
103
+ else
104
+ callbacks[:failure].call(response) if callbacks[:failure]
105
+ end
106
+ response.body if response.is_a?(Net::HTTPSuccess)
107
+ end
108
+ end
109
+
110
+ class Target
111
+ extend Helpers
112
+ end
113
+
114
+ class Action
115
+ extend Helpers
116
+
117
+ def self.check_args(params, all_names, required_names)
118
+ raise ArgumentError, "Invalid arguments: %s" % (params.keys - all_names).join(", "), caller if (params.keys - all_names).count > 0
119
+ raise ArgumentError, "Missing arguments: %s" % (required_names - params.keys).join(", "), caller if (required_names - params.keys).count > 0
120
+ end
121
+ end
122
+
123
+ def self.parse_action(name)
124
+ action, target = name.split(/([A-Z].*)/, 2)
125
+ target = "User" if %w(login logout).include?(action)
126
+ target = target.singularize
127
+ [action, target]
128
+ end
129
+
130
+ def self.load_config(api_xml)
131
+ contents = File.read(api_xml).to_s
132
+ md5 = Digest::MD5.hexdigest(contents)
133
+ api_xml_basename = File.basename(api_xml)
134
+ md5_file = Dir::home() + "/.#{api_xml_basename}.md5"
135
+ cache_file = Dir::home() + "/.#{api_xml_basename}.cache"
136
+ if File::exists?(md5_file) && File::read(md5_file).to_s == md5 && File::exists?(cache_file)
137
+ config = Marshal.restore(File::read(cache_file))
138
+ else
139
+ config = Nori.parse(contents)
140
+ File::open(md5_file, "w+") do |file|
141
+ file.print md5
142
+ end
143
+ File::open(cache_file, "w+") do |file|
144
+ file.print Marshal.dump(config)
145
+ end
146
+ end
147
+ config
148
+ end
149
+
150
+ def self.configure(options = {})
151
+ Arcus.log = Logger.new(STDOUT)
152
+ Arcus.log.level = Logger::DEBUG
153
+ Arcus.log.formatter = proc do |severity, datetime, progname, msg|
154
+ "#{severity} - #{datetime}: #{msg}\n"
155
+ end
156
+ api_xml = options[:api_xml] || (raise ArgumentError, "api_xml file: commands.xml file location required")
157
+ api_uri = options[:api_uri] || (raise ArgumentError, "api_uri: URI of API required")
158
+ @@default_response = options[:default_response] if options[:default_response]
159
+ to_name = lambda { |a| a["name"].to_sym }
160
+
161
+ config = self.load_config(api_xml)
162
+
163
+ config["commands"]["command"].each do |c|
164
+ command_name = c["name"]
165
+ command_desc = c["description"]
166
+ command_async = c["isAsync"] == "true" ? true : false
167
+
168
+ action, target = parse_action(command_name)
169
+
170
+ if c["request"] != nil
171
+ all_args = c["request"]["arg"] || []
172
+ else
173
+ all_args = []
174
+ end
175
+
176
+ if all_args.is_a?(Hash)
177
+ all_args = [all_args]
178
+ end
179
+ all_args.push({"name" => "response", "description" => "valid response types are yaml, xml, prettyxml, json, prettyjson (json is default)", "required" => "false"})
180
+
181
+ required_args = all_args.select { |a| a["required"] == "true" }
182
+ optional_args = all_args.select { |a| a["required"] == "false" }
183
+
184
+ all_names = all_args.map(&to_name)
185
+ required_names = required_args.map(&to_name)
186
+
187
+ def self.class_exists?(class_name)
188
+ c = self.const_get(class_name)
189
+ return c.is_a?(Class)
190
+ rescue NameError
191
+ return false
192
+ end
193
+
194
+ def self.get_class(class_name)
195
+ self.const_get(class_name)
196
+ end
197
+
198
+ def self.create_class(class_name, superclass, &block)
199
+ c = Class.new superclass, &block
200
+ self.const_set class_name, c
201
+ c
202
+ end
203
+
204
+ if !self.class_exists?(target)
205
+ target_clazz = self.create_class(target, Target) do
206
+ cattr_accessor :actions, :opts
207
+ self.actions = self.opts = []
208
+ end
209
+ @@targets << target_clazz
210
+ else
211
+ target_clazz = self.get_class(target)
212
+ end
213
+
214
+
215
+ target_clazz.class_eval do
216
+ if !class_exists?(action.capitalize)
217
+ action_clazz = create_class(action.capitalize, Action) do
218
+ cattr_accessor :opts
219
+ self.opts = []
220
+ end
221
+ target_clazz.actions << action_clazz
222
+ else
223
+ action_clazz = get_class(action.capitalize)
224
+ end
225
+ action_clazz.class_eval do
226
+ class << self
227
+ attr_accessor :name, :description, :is_async,
228
+ :required_args, :optional_args,
229
+ :action_name, :api_uri, :sync
230
+
231
+ def prepare!(params = {}, callbacks = {})
232
+ check_args(params, all_names, required_names)
233
+ prepare(params, callbacks)
234
+ end
235
+
236
+ def prepare(params = {}, callbacks = {})
237
+ params[:response] ||= Api.default_response
238
+ Request.new(self.name, params, self.api_uri, callbacks)
239
+ end
240
+ end
241
+ end
242
+ action_clazz.name = command_name
243
+ action_clazz.description = command_desc
244
+ action_clazz.action_name = action
245
+ action_clazz.required_args = required_args
246
+ action_clazz.optional_args = optional_args
247
+ action_clazz.api_uri = api_uri
248
+ action_clazz.is_async = command_async
249
+ action_clazz.sync = false
250
+
251
+
252
+ define_method(action.to_sym) do |params = {}, callbacks = {}|
253
+ action_clazz.prepare(params, callbacks)
254
+ end
255
+ define_method("#{action}!".to_sym) do |params = {}, callbacks = {}|
256
+ action_clazz.prepare(params, callbacks)
257
+ end
258
+
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end