arcus 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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