diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..4f662dc --- /dev/null +++ b/Gemfile @@ -0,0 +1,18 @@ +source ENV['GEM_SOURCE'] || 'https://rubygems.org' + +puppetversion = ENV.key?('PUPPET_VERSION') ? ENV['PUPPET_VERSION'] : ['>= 3.3'] +gem 'metadata-json-lint' +gem 'puppet', puppetversion +gem 'puppetlabs_spec_helper', '>= 1.0.0' +gem 'puppet-lint', '>= 1.0.0' +gem 'facter', '>= 1.7.0' +gem 'rspec-puppet' + +# rspec must be v2 for ruby 1.8.7 +if RUBY_VERSION >= '1.8.7' && RUBY_VERSION < '1.9' + gem 'rspec', '~> 2.0' + gem 'rake', '~> 10.0' +else + # rubocop requires ruby >= 1.9 + gem 'rubocop' +end diff --git a/README.md b/README.md new file mode 100644 index 0000000..bea5429 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +## PureStorage Puppet Module + +[![Puppet Forge](http://img.shields.io/puppetforge/v/pure/pure.svg)](https://forge.puppetlabs.com/pure/pure) +**Note: This address will be effective once the module is published on puppetforge. + +#### Table of Contents + + 1. [Disclaimer](#disclaimer) + 2. [Overview](#overview) + 3. [Description](#description) + 4. [Setup](#setup) + * [Connecting to a PureStorage Array](#connecting-to-a-purestorage-array) + 5. [Usage](#usage) + * [Puppet Device](#puppet-device) + * [Puppet Agent](#puppet-agent) + * [Puppet Apply](#puppet-apply) + 6. [Supported use-cases](#supported-use-cases) + 7. [Limitations](#limitations) + 8. [Development](#development) + +## Disclaimer + +This provider is written as best effort and provides no warranty expressed or +implied. Please contact the author(s) via [Pure Storage Support Team](https://www.purestorage.com/support.html) if you have +questions about this module before running or modifying. + +## Overview + +The PureStorage provider allows you to provision volumes on a PureStorage array +from either a puppet client or a puppet device proxy host. The provider has +been developed against CentOS 7.2 using Puppet-4.8.1. At this stage testing +is completely manual. + +## Description + +Using the `volume`, `hostconfig` and `connection` types, one +can quickly provision remote storage and attach it via iSCSI from a +PureStorage array to a client. + +The provider utilizes the robust REST API (V1.6) available on the PureStorage +array to remotely provision the necessary resources. + +## Setup + +### Connecting to a PureStorage Array + +A connection to a PureStorage array is via the storage array IP address +or FQDN name of the storage array and through use of a Admin account. +A connection string is needed to inform the providers how to connect. +The providers can get the connection string from various locations +(see Usage below) but the three pieces of information necessary are: + + 1. Admin account user name. + 2. Admin account password. + 3. IP address or DNS name. + +If multiple connection options are provided to the provider, it will use them +in the following order: + + 1. Any existing connection. + 2. A Facter-supplied URL. + 3. A user-supplied URL (in device.conf or site.pp file). + +## Usage + +### Puppet Device + +The Puppet Network Device system is a way to configure devices' (switches, +routers, storage) which do not have the ability to run puppet agent on +the devices. The device application acts as a smart proxy between the Puppet +Master and the managed network device. To do this, puppet device will +sequentially connects to the master on behalf of the managed network device +and will ask for a catalog (a catalog containing only network device +resources). It will then apply this catalog to the said device by translating +the resources to orders the network device understands. Puppet device will +then report back to the master for any changes and failures as a standard node. + +The PureStorage providers are designed to work with the puppet device concept and +in this case will retrieve their connection information from the `url` given +in Puppet's `device.conf` file. An example is shown below: + + [array1.puretec.purestorage.com] + type pure + url https://:@puretec.purestorage.com + +In the case of Puppet Device connection to the PureStorage is from the machine +running 'device' only. + +command : "puppet device" + +### Puppet Agent + +Puppet agent is the client/slave side of the puppet master/slave relationship. +In the case of puppet agent the connection information needs to be included in +the manifest supplied to the agent from the master or it could be included +in a custom fact passed to the client. The connection string must be supplied +as a URL. See the example manifests (complete_create.pp) for details. + +In the case of Puppet Agent, connections to the PureStorage array will be +initiated from every machine which utilizes the PureStorage puppet module this +way. This may be of security concern for some folks. + +Command: "puppet agent -t" + +### Puppet Apply + +Puppet apply is the client only application of a local manifest. Puppet apply +is supported similar to puppet agent by the PureStorage providers. +The connection string must be supplied as a URL. See the example +manifests (complete_create.pp) for details. + +Command: "puppet apply " + e.g. "puppet apply /etc/puppetlabs/code/environments/production/manifests/site.pp" + +## Supported use-cases: + + 1. create \ update \ delete volume + * Array of iqn-list supported + eg. host_iqnlist => ["iqn.1994-04.jp.co.pure:rsd.d9s.t.10103.0e03j","iqn.1994-04.jp.co.pure:rsd.d9s.t.10103.0e03k"], + ** volume size cannot be reduced REST API constraint. + 2. create \ update \ delete host + 3. create \ delete connection + +## Limitations + +Today pure puppet module supports create, update, delete of +volume ,host and direct connection (between the two). +Today, it supports IQN ids for iSCSI only. + +## Development + +Please see the [Pure Storage](https://www.purestorage.com/support.html) for any issues, +discussion, advice or contribution(s). + +To get started with developing this module, you'll need a functioning Ruby installation. \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..02609e3 --- /dev/null +++ b/Rakefile @@ -0,0 +1,32 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' +require 'metadata-json-lint/rake_task' + +if RUBY_VERSION >= '1.9' + require 'rubocop/rake_task' + RuboCop::RakeTask.new +end + +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.relative = true +PuppetLint.configuration.ignore_paths = ['spec/**/*.pp', 'pkg/**/*.pp'] + +desc 'Validate manifests, templates, and ruby files' +task :validate do + Dir['manifests/**/*.pp'].each do |manifest| + sh "puppet parser validate --noop #{manifest}" + end + Dir['spec/**/*.rb', 'lib/**/*.rb'].each do |ruby_file| + sh "ruby -c #{ruby_file}" unless ruby_file =~ %r{spec/fixtures} + end + Dir['templates/**/*.erb'].each do |template| + sh "erb -P -x -T '-' #{template} | ruby -c" + end +end + +desc 'Run metadata_lint, lint, validate, and spec tests.' +task :test do + [:metadata_lint, :lint, :validate, :spec].each do |test| + Rake::Task[test].invoke + end +end diff --git a/checksums.json b/checksums.json new file mode 100644 index 0000000..e1664f0 --- /dev/null +++ b/checksums.json @@ -0,0 +1,28 @@ +{ + "Gemfile": "05686b4d222246ee37fcb5514d3166e2", + "README.md": "6fa4fcba8403306746649807540a0130", + "Rakefile": "4be9ada03614912ceae1d51027a9faec", + "examples/complete_create.pp": "1c8ecab61ff14468bacb1f12745adb30", + "examples/connection.pp": "f892ca40beaec8b01bc7251cb12d8372", + "examples/hostconfig.pp": "b2d298545b12ac68de76d6734cd85635", + "examples/init.pp": "666a7b7a7486f1d71981a8a0decb72b7", + "examples/volume.pp": "5a9f3745218b126d1fbb451c1fc64187", + "lib/augeas/lenses/puppet_device.aug": "191267b73531554d146c9c196b30b702", + "lib/puppet/cacheservice.rb": "1a78e91c1c62f00aa8bf8ddc0f52d1a1", + "lib/puppet/provider/connection/connection.rb": "48ae7b8c93c0ebcad7733d31d8a4c2a1", + "lib/puppet/provider/hostconfig/hostconfig.rb": "9883a6c52b28790a0649f65d113cbe52", + "lib/puppet/provider/pure.rb": "fbe8984844ef41ad63ff18b65ed16d77", + "lib/puppet/provider/volume/volume.rb": "b440714049c83ac6d65809baad24ac4c", + "lib/puppet/purestorage_api.rb": "19b58dcc50d6da1688371f7190230970", + "lib/puppet/type/connection.rb": "2bfae550902a8e5270787f0d74f1f7a6", + "lib/puppet/type/hostconfig.rb": "91d5830b03f27b2f668764a92bed5297", + "lib/puppet/type/volume.rb": "2afcce9a25baf20f3564f912191ac6e8", + "lib/puppet/util/network_device/pure/device.rb": "c86300ab8e60a23b36f9366bc78c3f30", + "lib/puppet/util/network_device/pure/facts.rb": "70ce046598f5c98dccee3b8035bfcd2a", + "lib/puppet/util/network_device/pure.rb": "a809390ab73d128cbb76249482c771f7", + "manifests/device.pp": "ad19044e48504a749152944dace74c90", + "manifests/init.pp": "fc23be1365eeac5a747ac97b0c014b5c", + "metadata.json": "82767efd58f926799e262095d2318330", + "spec/classes/init_spec.rb": "c2dd3a700793ef534b0a5543931f19d3", + "spec/spec_helper.rb": "74085ad4d8ee01166015b10a843ee959" +} \ No newline at end of file diff --git a/examples/complete_create.pp b/examples/complete_create.pp new file mode 100644 index 0000000..6af9774 --- /dev/null +++ b/examples/complete_create.pp @@ -0,0 +1,85 @@ +#Example of Puppet Device +node 'cloud-dev-405-a12-02.puretec.purestorage.com' { #--> This is Device name + volume{ "pure_storage_volume": + #ensure either "present" or "absent" + ensure => "present", + volume_name => "test_device_volume", + volume_size => "2.0G", + } + hostconfig{ "pure_storage_host": + ensure => "present", + host_name => "test-device-host", + host_iqnlist => ["iqn.1994-04.jp.co.pure:rsd.d9s.t.10103.0e03f","iqn.1994-04.jp.co.pure:rsd.d9s.t.10103.0e03g"], + } + connection{ "pure_storage_connection": + ensure => "present", + host_name => "test-device-host", + volume_name => "test_device_volume", + #Added dependency on volume and host resource types + #to be present, other wise connection will fail. + require => [Volume['pure_storage_volume'], + Hostconfig['pure_storage_host'] + ], + } + +} +#Example of Puppet Agent +node 'calsoft-puppet-agent.puretec.purestorage.com'{ #--> This is Agent vm name + #Note : device_url is MANDATORY here. + $device_url = 'https://pureuser:pureuser@cloud-dev-405-a12-02.puretec.purestorage.com' + + volume{ "pure_storage_volume": + #ensure either "present" or "absent" + ensure => "present", + volume_name => "test_agent_volume", + volume_size => "1.0G", + device_url => $device_url, + } + hostconfig{ "pure_storage_host": + ensure => "present", + host_name => "test-agent-host", + host_iqnlist => ["iqn.1994-04.jp.co.pure:rsd.d9s.t.10103.0e03h","iqn.1994-04.jp.co.pure:rsd.d9s.t.10103.0e0i"], + device_url => $device_url, + } + connection{ "pure_storage_connection": + ensure => "present", + host_name => "test-agent-host", + volume_name => "test_agent_volume", + #Added dependency on volume and host resource types + #to be present, other wise connection will fail. + require => [Volume['pure_storage_volume'], + Hostconfig['pure_storage_host'] + ], + device_url => $device_url, + } +} +#Example of Puppet Apply +node 'puppet.puretec.purestorage.com'{ #--> This is master vm name + #Note: device_url is MANDATORY here. + $device_url = 'https://pureuser:pureuser@cloud-dev-405-a12-02.puretec.purestorage.com' + + volume{ "pure_storage_volume": + #ensure either "present" or "absent" + ensure => "present", + volume_name => "test_apply_volume", + volume_size => "1.0G", + device_url => $device_url, + } + hostconfig{ "pure_storage_host": + ensure => "present", + host_name => "test-apply-host", + host_iqnlist => ["iqn.1994-04.jp.co.pure:rsd.d9s.t.10103.0e03j","iqn.1994-04.jp.co.pure:rsd.d9s.t.10103.0e03k"], + device_url => $device_url, + } + connection{ "pure_storage_connection": + ensure => "present", + host_name => "test-apply-host", + volume_name => "test_apply_volume", + #Added dependency on volume and host resource types + #to be present, other wise connection will fail. + require => [Volume['pure_storage_volume'], + Hostconfig['pure_storage_host'] + ], + device_url => $device_url, + } +} diff --git a/examples/connection.pp b/examples/connection.pp new file mode 100644 index 0000000..d1a49da --- /dev/null +++ b/examples/connection.pp @@ -0,0 +1,8 @@ + node 'cloud-dev-405-a12-02.puretec.purestorage.com' { + + connection{ "pure_storage_connection": + ensure => "absent", + host_name => "test-host", + volume_name => "test_02", + } +} \ No newline at end of file diff --git a/examples/hostconfig.pp b/examples/hostconfig.pp new file mode 100644 index 0000000..006ca38 --- /dev/null +++ b/examples/hostconfig.pp @@ -0,0 +1,8 @@ + node 'cloud-dev-405-a12-02.puretec.purestorage.com' { + + hostconfig{ "pure_storage_host": + ensure => "absent", + host_name => "test-host", + host_iqnlist => "iqn.1994-04.jp.co.hitachi:rsd.d9s.t.10103.0e02a", + } +} \ No newline at end of file diff --git a/examples/init.pp b/examples/init.pp new file mode 100644 index 0000000..4afe69a --- /dev/null +++ b/examples/init.pp @@ -0,0 +1,12 @@ +# The baseline for module testing used by Puppet Labs is that each manifest +# should have a corresponding test manifest that declares that class or defined +# type. +# +# Tests are then run by using puppet apply --noop (to check for compilation +# errors and view a log of events) or by fully applying the test in a virtual +# environment (to compare the resulting system state to the desired state). +# +# Learn more about module testing here: +# https://docs.puppet.com/guides/tests_smoke.html +# +include ::pure diff --git a/examples/volume.pp b/examples/volume.pp new file mode 100644 index 0000000..61c7657 --- /dev/null +++ b/examples/volume.pp @@ -0,0 +1,8 @@ + node 'cloud-dev-405-a12-02.puretec.purestorage.com' { + + volume{ "pure_storage_volume": + ensure => "absent", + volume_name => "test_02", + volume_size => "1.0G", + } +} \ No newline at end of file diff --git a/lib/augeas/lenses/puppet_device.aug b/lib/augeas/lenses/puppet_device.aug new file mode 100644 index 0000000..7c1bf37 --- /dev/null +++ b/lib/augeas/lenses/puppet_device.aug @@ -0,0 +1,101 @@ +(* -*- coding: utf-8 -*- +Module: Puppet_Device + Parses /etc/puppetlabs/puppet/device.conf used by a puppet node. + +Author: + Joshua M. Keyes + Frédéric Lespez + +About: Reference + This lens tries to keep as close as possible to the puppet documentation for this file: + + http://docs.puppetlabs.com/puppet/4.3/reference/config_file_device.html + + This lens was based heaily off of the 'PuppetFileserver' lens. + +About: License + This file is licensed under the LGPL v2+, like the rest of Augeas. + +About: Lens Usage + Nothing to see here yet. + +About: Configuration Files + This lens applies to /etc/puppetlabs/puppet/device.conf. See . +*) + +module Puppet_Device = + autoload xfm + +(************************************************************************ + * Group: USEFUL PRIMITIVES + *************************************************************************) + +(* Group: INI File settings *) + +(* Variable: eol *) +let eol = IniFile.eol + +(* Variable: sep + Only treat one space as the sep, extras are stripped by IniFile *) +let sep = Util.del_str " " + +(* +Variable: comment + Only supports "#" as commentary + *) +let comment = IniFile.comment "#" "#" + +(* +Variable: entry_re + Regexp for possible keyword (type, url) + *) +let entry_re = /type|url/ + +(************************************************************************ + * Group: ENTRY + *************************************************************************) + +(* +View: entry + - It might be indented with an arbitrary amount of whitespace + - It does not have any separator between keywords and their values + - It can only have keywords with the following values (type, url) +*) +let entry = IniFile.indented_entry entry_re sep comment + + +(************************************************************************ + * Group: RECORD + *************************************************************************) + +(* Group: Title definition *) + +(* +View: title + Uses standard INI File title +*) +let title = IniFile.indented_title IniFile.record_re + +(* +View: title + Uses standard INI File record +*) +let record = IniFile.record title entry + + +(************************************************************************ + * Group: LENS + *************************************************************************) + +(* +View: lns + Uses standard INI File lens +*) +let lns = IniFile.lns record comment + +(* Variable: filter *) +let filter = (incl "/etc/puppet/device.conf" + .incl "/usr/local/etc/puppet/device.conf" + .incl "/etc/puppetlabs/puppet/device.conf") + +let xfm = transform lns filter diff --git a/lib/puppet/cacheservice.rb b/lib/puppet/cacheservice.rb new file mode 100644 index 0000000..71ff172 --- /dev/null +++ b/lib/puppet/cacheservice.rb @@ -0,0 +1,105 @@ +#===================================== +#This class will provide methods to +# deal with Cache service to cache +# Session keys , tokens which are required +# in REST API calls in Pure Storage Array. +# +# +# Supports REST API 1.6 +#===================================== + +require 'tmpdir' + +class CacheService + + CACHE_EXPIRY = 25 # in minutes + + #------------------------------------------------------------------------------------ + # Get the OS specific temp dir location + # Cache data will be stored in /tmp/ file. + #------------------------------------------------------------------------------------ + def getOsTmpDir + tmpFileName = Dir.tmpdir() + Puppet.debug("@@@@@ OS specific tmp file path is : "+ tmpFileName) + return tmpFileName + end + + #------------------------------------------------------------------------------------ + # Constructor + #------------------------------------------------------------------------------------ + def initialize (cache_file_name) + @cache_file_name = getOsTmpDir() + "/"+ cache_file_name + Puppet.debug("@@@@ Cache file path is : "+ @cache_file_name) + end + + #------------------------------------------------------------------------------------ + # Below method will check if cache file is expired by comparing it with CACHE_EXPIRY + #------------------------------------------------------------------------------------ + def isCacheExpired + if(File.exist?(@cache_file_name)) + file_create_time = File.mtime(@cache_file_name) + current_time = Time.now + #Find out time difference in minutes + time_difference = (current_time-file_create_time)/60 + #puts time_difference + #Expiry is 25 minutes + return true if(time_difference > CACHE_EXPIRY) + end + return false + end + + #------------------------------------------------------------------------------------ + # Below method will cache properties like token, session + # This method will save or updates key,value in file + #------------------------------------------------------------------------------------ + def writeCache (key,value) + begin + aFile = File.new(@cache_file_name, "a+") + aFile.puts(key+":"+value) + aFile.close + rescue + #puts "File '"+ @cache_file_name +"' not found or some other exception in writeCache()!!!" + Puppet.debug("File '"+ @cache_file_name +"' not found or some other exception in writeCache()!!!") + return nil + end + end + + #------------------------------------------------------------------------------------ + # Below method will return cache properties like token, session from file + #------------------------------------------------------------------------------------ + def readCache(key) + begin + lines = File.readlines(@cache_file_name) + line_num=0 + while line_num < lines.size do + if(lines[line_num]!= nil and lines[line_num].strip.index(key)==0 and lines[line_num].strip.include? key) + #puts "Key " + key + " is found!!!" + Puppet.debug("Key " + key + " is found!!!") + value = lines[line_num].strip.split(":")[1] + return value + end + line_num = + 1 + end + return nil + rescue + #puts "File '"+ @cache_file_name +"' not found or key not found in cache readCache()!!!" + Puppet.debug("File '"+ @cache_file_name +"' not found or key not found in cache readCache()!!!") + return nil + end + end + + #------------------------------------------------------------------------------------ + # Below method will delete file if it exists + #------------------------------------------------------------------------------------ + def deleteCache + begin + File.delete(@cache_file_name) if File::exist?(@cache_file_name) + #puts "File '"+ @cache_file_name +"' deleted successfully!!!" + Puppet.debug("File '"+ @cache_file_name +"' deleted successfully!!!") + rescue + #puts "File '"+ @cache_file_name +"' not found or some other exception in deleteCache()!!!" + Puppet.debug("File '"+ @cache_file_name +"' not found or some other exception in deleteCache()!!!") + return nil + end + end +end diff --git a/lib/puppet/provider/connection/connection.rb b/lib/puppet/provider/connection/connection.rb new file mode 100644 index 0000000..8329470 --- /dev/null +++ b/lib/puppet/provider/connection/connection.rb @@ -0,0 +1,47 @@ +require 'net/http' +require 'puppet/purestorage_api' +require 'puppet/provider/pure' +require 'puppet/util/network_device' +require 'puppet/util/network_device/pure/device' + +Puppet::Type.type(:connection).provide(:connection, + :parent => Puppet::Provider::Pure) do + desc "This is a provider for creating private connection between host and volume." + + def create + Puppet.debug("<<<<<<<<<< Inside connection create") + transport.executeConnectionRestApi(self.class::CREATE,@host_name,@volume_name) + end + + def destroy + Puppet.debug("<<<<<<<<<< Inside connection destroy") + transport.executeConnectionRestApi(self.class::DELETE,@host_name,@volume_name) + end + + def exists? + Puppet.debug("<<<<<<<<<< Inside connection exists?") + @host_name = resource[:host_name] + @volume_name = resource[:volume_name] + @url = resource[:device_url] + + Puppet.debug("host_name :" + @host_name) + Puppet.debug("volume_name :" + @volume_name) + Puppet.debug("url :" + @url.to_s) + + #Set FACT for "url" + if(!@url.to_s.nil?) + command_echo = 'echo '+@url.to_s + Facter.add(:url) do + setcode command_echo + end + end + + #Check connection existence + isExists = transport.isConnectionExists(@host_name,@volume_name) + + Puppet.info("\n Is connection between host :'"+@host_name+"' and volume: '"+ @volume_name +"' exists? "+ isExists.to_s) + return isExists + end +end + + diff --git a/lib/puppet/provider/hostconfig/hostconfig.rb b/lib/puppet/provider/hostconfig/hostconfig.rb new file mode 100644 index 0000000..a4f481f --- /dev/null +++ b/lib/puppet/provider/hostconfig/hostconfig.rb @@ -0,0 +1,65 @@ +require 'net/http' +require 'puppet/purestorage_api' +require 'puppet/provider/pure' +require 'puppet/util/network_device' +require 'puppet/util/network_device/pure/device' + +Puppet::Type.type(:hostconfig).provide(:hostconfig, + :parent => Puppet::Provider::Pure) do + desc "Provider for PureStorage host." + + def create + Puppet.debug("<<<<<<<<<< Inside hostconfig create & operation:"+@operation) + transport.executeHostRestApi(@operation,@host_name,@host_iqnlist) + end + + def destroy + Puppet.debug("<<<<<<<<<< Inside hostconfig destroy & operation:"+@operation) + transport.executeHostRestApi(@operation,@host_name,@host_iqnlist) + end + + def exists? + Puppet.debug("<<<<<<<<<< Inside hostconfig exists?") + @host_name = resource[:host_name] + @host_iqnlist = resource[:host_iqnlist] + @ensure = resource[:ensure] + @url = resource[:device_url] + + #@host_wwnlist = resource[:host_wwnlist] + Puppet.debug "host_name :" + @host_name + Puppet.debug "host_iqnlist :" + @host_iqnlist.to_s + Puppet.debug "ensure :" + @ensure.to_s + #Puppet.debug "host_wwnlist :" + @host_wwnlist + Puppet.debug "url :" + @url.to_s + + # Set FACT for URL + if(!@url.to_s.nil?) + command_echo = 'echo '+@url.to_s + Facter.add(:url) do + setcode command_echo + end + end + + #Check host existence + isExists = transport.isHostExists(@host_name,@host_iqnlist) + + Puppet.info("\n Is host: '"+@host_name+"' exists? "+ isExists.to_s) + + #Decide which operation to do Create\Update\Delete + if(@ensure == :present) + if(isExists) + @operation= self.class::UPDATE #"update" + isExists = false + else + @operation= self.class::CREATE #"create" + end + elsif(@ensure == :absent) + @operation= self.class::DELETE #"delete" + end + + Puppet.debug("<<<<<<<<<< Operation to perform? "+ @operation) + return isExists + end +end + + diff --git a/lib/puppet/provider/pure.rb b/lib/puppet/provider/pure.rb new file mode 100644 index 0000000..9680fab --- /dev/null +++ b/lib/puppet/provider/pure.rb @@ -0,0 +1,35 @@ +#==================================================================== +# Disclaimer: This script is written as best effort and provides no +# warranty expressed or implied. Please contact the author(s) if you +# have questions about this script before running or modifying +#==================================================================== + +require 'puppet/provider' +require 'puppet/util/network_device' +require 'puppet/util/network_device/pure/device' + +class Puppet::Provider::Pure < Puppet::Provider + CREATE = "create" + UPDATE = "update" + DELETE = "delete" + + def self.transport(args=nil) + @device ||= Puppet::Util::NetworkDevice.current + if not @device and Facter.value(:url) + Puppet.debug "@@@@ NetworkDevice::Pure: connecting via facter url." + @device ||= Puppet::Util::NetworkDevice::Pure::Device.new(Facter.value(:url)) + elsif not @device and args and args.length == 1 + Puppet.debug "@@@@ NetworkDevice::Pure: connecting via argument bits #{args[0]}." + @device ||= Puppet::Util::NetworkDevice::Pure::Device.new(args[0]) + end + raise Puppet::Error, "#{self.class} : device not initialized " \ + "#{caller.join("\n")}" unless @device + @transport = @device.transport + end + + def transport(*args) + # this calls the class instance of self.transport instead of the object + # instance which causes an infinite loop. + self.class.transport(args) + end +end diff --git a/lib/puppet/provider/volume/volume.rb b/lib/puppet/provider/volume/volume.rb new file mode 100644 index 0000000..46f2a8f --- /dev/null +++ b/lib/puppet/provider/volume/volume.rb @@ -0,0 +1,64 @@ +require 'net/http' +require 'facter' + +require 'puppet/purestorage_api' +require 'puppet/provider/pure' +require 'puppet/util/network_device' +require 'puppet/util/network_device/pure/device' + +Puppet::Type.type(:volume).provide(:volume, + :parent => Puppet::Provider::Pure) do + desc "Provider for type PureStorage volume." + + def create + Puppet.debug("<<<<<<<<<< Inside volume create & operation:"+@operation) + transport.executeVolumeRestApi(@operation,@volume_name,@volume_size) + end + + def destroy + Puppet.debug("<<<<<<<<<< Inside volume destroy & operation:"+@operation) + transport.executeVolumeRestApi(@operation,@volume_name,@volume_size) + end + + def exists? + Puppet.debug("<<<<<<<<<< Inside volume exists?") + @volume_name = resource[:volume_name] + @volume_size = resource[:volume_size] + @ensure = resource[:ensure] + @url = resource[:device_url] + + Puppet.debug "volume_name :" + @volume_name + Puppet.debug "volume_size :" + @volume_size + Puppet.debug "ensure :" + @ensure.to_s + Puppet.debug "url :" + @url.to_s + + # Set FACT for URL + if(!@url.to_s.nil?) + command_echo = 'echo '+@url.to_s + Facter.add(:url) do + setcode command_echo + end + end + + #Check volume existence + isExists = transport.isVolumeExists(@volume_name,@volume_size) + Puppet.info("\n Is volume:'"+@volume_name+"' exists? "+ isExists.to_s) + + #Decide which operation to do Create\Update\Delete + if(@ensure == :present) + if(isExists) + @operation= self.class::UPDATE #"update" + isExists = false + else + @operation= self.class::CREATE #"create" + end + elsif(@ensure == :absent) + @operation= self.class::DELETE #"delete" + end + + Puppet.debug("<<<<<<<<<< Operation to perform? "+ @operation) + return isExists + end +end + + diff --git a/lib/puppet/purestorage_api.rb b/lib/puppet/purestorage_api.rb new file mode 100644 index 0000000..552d452 --- /dev/null +++ b/lib/puppet/purestorage_api.rb @@ -0,0 +1,463 @@ +#===================================== +#This class is mainly used for +#REST API 1.6 for Pure Storage Array +#It will have utility methods +#to perform CRUD operations on +#Volume and Host and creating +#connection between them. +# +# Supports REST API 1.6 +#===================================== + +require 'net/https' +require 'uri' +require 'json' +require 'puppet/cacheservice' + +class PureStorageApi + + CONTENT_TYPE = "Content-Type" + APPLICATION_JSON = "application/json" + COOKIE = "Cookie" + TOKEN = "TOKEN" + SESSION_KEY = "SESSION_KEY" + REST_VERSION = "1.6" + CREATE = "create" + UPDATE = "update" + DELETE = "delete" + LIST = "list" + + attr_accessor :url + + #------------------------------------------------------------------------------------ + # Constructor + #------------------------------------------------------------------------------------ + def initialize(url) + Puppet.debug("Device url : "+ url) + @url = URI.parse(url) + @deviceIp = @url.host + Puppet.debug("Device ip : "+ @deviceIp) + @userName = @url.user + Puppet.debug("Device user : "+ @userName) + @password = @url.password + Puppet.debug("Device password : "+ @password) + @restVersion = REST_VERSION + #Base URI is ... https://m70.purecloud.local/api/1.6 + @baseUri = "https://" + @deviceIp + "/api/"+@restVersion + @cacheService = CacheService.new(@deviceIp) + + #Delete Cache if its expired. + if(@cacheService.isCacheExpired) + # puts "Cache is expired, hence deleting file :" + @deviceIp + Puppet.debug "Cache is expired, hence deleting file :" + @deviceIp + @cacheService.deleteCache() + end + end + #------------------------------------------------------------------------------------ + # Constructor + #------------------------------------------------------------------------------------ +# def initialize(deviceIp,userName,password,restVersion) +# @deviceIp = deviceIp +# @userName = userName +# @password = password +# @restVersion = restVersion +# #Base URI is ... https://m70.purecloud.local/api/1.6 +# @baseUri = "https://" + deviceIp + "/api/"+restVersion +# @cacheService = CacheService.new(deviceIp) +# +# #Delete Cache if its expired. +# if(@cacheService.isCacheExpired) +# # puts "Cache is expired, hence deleting file :" + @deviceIp +# Puppet.debug "Cache is expired, hence deleting file :" + @deviceIp +# @cacheService.deleteCache() +# end +# end + + + + #------------------------------------------------------------------------------------ + # Step 1 : Create Token + # e.g. + # POST https://m70.purecloud.local/api/1.6/auth/apitoken + # + # This method returns token generated by REST server which is used to create session + #------------------------------------------------------------------------------------ + def createToken + token = nil + + begin + token = @cacheService.readCache(TOKEN) + # puts "Found Token : " + token + Puppet.debug("Found Token : " + token) + rescue + Puppet.debug("Looks like token is not cashed earlier or some other issue!") + end + + if (token == nil) + #uri = URI.parse('https://m70.purecloud.local/api/1.6/auth/apitoken') + url = @baseUri + "/auth/apitoken" + uri = URI.parse(url) + + #Define Header here + #header = {'Content-Type'=> 'application/json'} + header = {CONTENT_TYPE => APPLICATION_JSON} + + # Create the HTTP objects + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + + #Get Request Object + request = Net::HTTP::Post.new(uri.request_uri, header) + + #Set Body for the request + #request.set_form_data('password' => 'pureuser', 'username' => 'pureuser') + request.set_form_data('password' => @password, 'username' => @userName) + begin + # Send the request + response = http.request(request) + parsed = JSON.parse(response.body) + token = parsed['api_token'] + #Store in Cache + @cacheService.writeCache(TOKEN,token) + rescue Exception + #puts "Device '"+@deviceIp + "' is either not reachable or down!!!" + Puppet.err("Device '"+@deviceIp + "' is either not reachable or down!!!") + #raise Exception + end + else + # puts "TODO Else of if token == nil" + end + + return token + end + + #---------------------------------------------------------------------------- + #Step 2: Create session by passing token obtained in createToken method + # e.g. + # POST https://m70.purecloud.local/api/1.6/auth/session + # + # This method returns session key which will be used in further rest calls + #---------------------------------------------------------------------------- + def createSession (token) + session_key = nil + + begin + session_key = @cacheService.readCache(SESSION_KEY) + # puts "Found session_key : " + session_key + Puppet.debug("Found session_key : " + session_key) + rescue + #puts "Looks like session is not cashed earlier or some other issue!" + Puppet.debug("Looks like session is not cashed earlier or some other issue!") + end + + if (session_key == nil) + #uri = URI.parse('https://m70.purecloud.local/api/1.6/auth/session') + url = @baseUri + "/auth/session" + uri = URI.parse(url) + + #Define Header here + #header = {'Content-Type'=> 'application/json'} + header = {CONTENT_TYPE => APPLICATION_JSON} + + # Create the HTTP objects + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + #Get Request Object + request = Net::HTTP::Post.new(uri.request_uri, header) + #Set Body for the request + #request.set_form_data('api_token' => '859024da-a74a-babd-a7a7-b9835f2b2aba') + request.set_form_data('api_token' => token) + begin + # Send the request + response = http.request(request) + # parsed header against 'Set-Cookie' contains session information + session_key = response.header['Set-Cookie'] + #puts session_key + #Store in Cache + @cacheService.writeCache(SESSION_KEY,session_key) + rescue Exception + #puts "Device '"+@deviceIp + "' is either not reachable or down!!!" + Puppet.err("Device '"+@deviceIp + "' is either not reachable or down!!!") + #raise Exception + end + else + # puts "TODO Else of if session_key == nil" + end + return session_key + end + + #------------------------------------------------- + # This method calls creates (token and session) + # e.g. + # https://pure01.example.com/api/1.6/volume + # return generated session + #------------------------------------------------- + def getSession + token = createToken() + if(token==nil) + raise "Unable to create a token for device: "+@deviceIp+". Please check the credentials or device Ip Address provided in the url!" + else + session = createSession(token) + end + + return session + end + + #------------------------------------------------- + # Generic method for GET requests + # e.g. + # GET https://pure01.example.com/api/1.6/volume + #------------------------------------------------- + def getRestCall(arg_url) + #uri = URI.parse('https://m70.purecloud.local/api/1.6/volume') + url = @baseUri + arg_url + uri = URI.parse(url) + + #get session + session = getSession + + #Pass Session in header + header = {CONTENT_TYPE => APPLICATION_JSON, COOKIE => session} + + # Create the HTTP objects + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + request = Net::HTTP::Get.new(uri.request_uri,header) + # Send the request + response = http.request(request) + + #puts response.body + if(response.body["pure_err_key"]==nil) + Puppet.info(response.body) + else + Puppet.err(response.body) + end + + return response.body + end + + #------------------------------------------------- + # Generic method for POST requests + # e.g. + # POST https://pure01.example.com/api/1.6/volume/v5 + # { + # "size": "5G" + # } + #------------------------------------------------- + def postRestCall(arg_url,arg_body) + + url = @baseUri + arg_url + uri = URI.parse(url) + + #get session + session = getSession + + #Pass Session in header + header = {CONTENT_TYPE => APPLICATION_JSON, COOKIE => session} + + # Create the HTTP objects + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + request = Net::HTTP::Post.new(uri.request_uri,header) + request.body = arg_body.to_json + # puts "@@@@@@@@: "+ request.body + + # Send the request + response = http.request(request) + + # puts response.body + if(response.body["pure_err_key"]==nil) + Puppet.info(response.body) + else + Puppet.err(response.body) + end + end + +#------------------------------------------------- + # Generic method for POST requests + # e.g. + # PUT https://pure01.example.com/api/1.6/volume/v5 + # { + # size: 10G + # } + #------------------------------------------------- + def putRestCall(arg_url,arg_body) + + url = @baseUri + arg_url + uri = URI.parse(url) + + #get session + session = getSession + + #Pass Session in header + header = {CONTENT_TYPE => APPLICATION_JSON, COOKIE => session} + + # Create the HTTP objects + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + request = Net::HTTP::Put.new(uri.request_uri,header) + request.body = arg_body.to_json + # puts "@@@@@@@@: "+ request.body + + # Send the request + response = http.request(request) + + #puts response.body + if(response.body["pure_err_key"]==nil) + Puppet.info(response.body) + else + Puppet.err(response.body) + end + end + +#------------------------------------------------- + # Generic method for delete requests + # e.g. + # POST https://pure01.example.com/api/1.6/volume/v5 + # { + # "size": "5G" + # } + #------------------------------------------------- + def deleteRestCall(arg_url) + + url = @baseUri + arg_url + uri = URI.parse(url) + + #get session + session = getSession + + #Pass Session in header + header = {CONTENT_TYPE => APPLICATION_JSON, COOKIE => session} + + # Create the HTTP objects + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + request = Net::HTTP::Delete.new(uri.request_uri,header) + + # Send the request + response = http.request(request) + end + + #---------------------------------------------------- + # This method checks if volume with given name exists + # It is dedicated to volumes + #----------------------------------------------- + def isVolumeExists(arg_volume_name, arg_volume_size) + url = "/volume/"+arg_volume_name + output = getRestCall(url) + + if(output["pure_err_key"]==nil) + return true + else + return false + end + end + + + #------------------------------------------------- + # Its a controller method which decides + # which rest api to call depending on key + # It is dedicated to volumes + #----------------------------------------------- + def executeVolumeRestApi(arg_key,*arg) + Puppet.info(arg_key + " Action for volume:"+ arg[0]) + case arg_key + when LIST then + getRestCall("/volume") + when CREATE then #arg[0] = volume_name, arg[1] = volume_size + url = "/volume/"+arg[0] + body = Hash.new("size" => arg[1]) + postRestCall(url,body["size"]) + when UPDATE then + url = "/volume/"+arg[0] + body = Hash.new("size" => arg[1]) + putRestCall(url,body["size"]) + when DELETE then + url = "/volume/"+arg[0] + deleteRestCall(url) + else + #puts "Invalid Option:" + arg_key + Puppet.err("Invalid Operation:" + arg_key + ", Available operations are [create,update,delete,list].") + end + end + + #---------------------------------------------------- + # This method checks if volume with given name exists + # It is dedicated to hosts + #----------------------------------------------- + def isHostExists(arg_host_name, arg_host_iqnlist) + url = "/host/"+arg_host_name + output = getRestCall(url) + + if(output["pure_err_key"]==nil) + return true + else + return false + end + end + #------------------------------------------------- + # Its a controller method which decides + # which rest api to call depending on key + # It is dedicated to Hosts + #----------------------------------------------- + def executeHostRestApi(arg_key,*arg) + Puppet.info(arg_key + " Action for host:"+ arg[0]) + case arg_key + when LIST then + getRestCall("/host") + when CREATE then #arg[0] = volume_name, arg[1] = volume_size + url = "/host/"+arg[0] + body = Hash.new("iqnlist" => arg[1]) + postRestCall(url,body["iqnlist"]) + when UPDATE then + url = "/host/"+arg[0] + body = Hash.new("iqnlist" => arg[1]) + putRestCall(url,body["iqnlist"]) + when DELETE then + url = "/host/"+arg[0] + deleteRestCall(url) + else + Puppet.err("Invalid Option:" + arg_key) + end + end + +#---------------------------------------------------- +# This method checks if connection with given name exists +# It is dedicated to volumes +# ----------------------------------------------- +def isConnectionExists(arg_host_name, arg_volume_name) + url = "/host/"+arg_host_name+"/volume" + output = getRestCall(url) + + if(output["vol"]!=nil) + return true + else + return false + end +end + #------------------------------------------------- + # Its a controller method which decides + # which rest api to call depending on key + # It is dedicated to Hosts + # arg[0] = hostname, arg[1] = volumename + #----------------------------------------------- + def executeConnectionRestApi(arg_key,*arg) + Puppet.info(arg_key + " Action for connection between host :"+arg[0]+" and volume:"+ arg[1]) + case arg_key + when CREATE then #arg[0] = volume_name, arg[1] = volume_size + url = "/host/"+arg[0]+"/volume/"+arg[1] + postRestCall(url,"") + when DELETE then + url = "/host/"+arg[0]+"/volume/"+arg[1] + deleteRestCall(url) + else + Puppet.err("Invalid Option:" + arg_key) + end + end +end diff --git a/lib/puppet/type/connection.rb b/lib/puppet/type/connection.rb new file mode 100644 index 0000000..ea14269 --- /dev/null +++ b/lib/puppet/type/connection.rb @@ -0,0 +1,29 @@ +Puppet::Type.newtype(:connection) do + @doc = "It does CRUD operations for Host-volume Connection on a Pure Storage flash array." + + validate do + self.fail "host name and volume name are mandatory" if !self[:host_name] || !self[:volume_name] + end + + apply_to_all + ensurable + + newparam(:host_name) do + desc "The name of the host. " + isnamevar + validate do |value| + fail("host name: #{value} can not be empty or null") if value == "null" or value.strip.empty? + end + end + + newparam(:volume_name) do + desc "The name of the volume." + validate do |value| + fail("volume name: #{value} can not be empty or null") if value == "null" or value.strip.empty? + end + end + + newparam(:device_url) do + desc "URL in the form of https://:@" + end + end diff --git a/lib/puppet/type/hostconfig.rb b/lib/puppet/type/hostconfig.rb new file mode 100644 index 0000000..110c961 --- /dev/null +++ b/lib/puppet/type/hostconfig.rb @@ -0,0 +1,29 @@ +Puppet::Type.newtype(:hostconfig) do + @doc = "It does CRUD operations for hosts on a Pure Storage flash array." + + apply_to_all + ensurable + + newparam(:host_name) do + desc "The name of the host." + isnamevar + validate do |value| + fail("host name can not be empty or null: #{value}") if value == "null" or value.strip.empty? + end + end + + newparam(:host_iqnlist) do + desc "The iqnlist" + validate do |value| + fail("host iqn_list can not be null string: #{value}") if value == "null" or value.to_s == '' + end + end + + newparam(:device_url) do + desc "URL in the form of https://:@" + end +# Not Supported for V1.0 +# newparam(:host_wwnlist) do +# desc "The wwnlist" +# end + end diff --git a/lib/puppet/type/volume.rb b/lib/puppet/type/volume.rb new file mode 100644 index 0000000..7feb498 --- /dev/null +++ b/lib/puppet/type/volume.rb @@ -0,0 +1,29 @@ +Puppet::Type.newtype(:volume) do + @doc = "It does CRUD operations for volumes on a Pure Storage flash array." + + validate do + self.fail "volume size attribute is mandatory" if !self[:volume_size] + end + + apply_to_all + ensurable + + newparam(:volume_name) do + desc "The name of the volume." + isnamevar + validate do |value| + fail("volume name can not be empty or null: #{value}") if value == "null" or value.strip.empty? + end + end + + newparam(:volume_size) do + desc "The volume size in GB e.g. 1G " + validate do |value| + fail("volume size can not be empty or null: #{value}") if value == "null" or value.strip.empty? + end + end + + newparam(:device_url) do + desc "URL in the form of https://:@" + end +end diff --git a/lib/puppet/util/network_device/pure.rb b/lib/puppet/util/network_device/pure.rb new file mode 100644 index 0000000..6bcbd67 --- /dev/null +++ b/lib/puppet/util/network_device/pure.rb @@ -0,0 +1,3 @@ +module Puppet::Util::NetworkDevice::Pure + +end diff --git a/lib/puppet/util/network_device/pure/device.rb b/lib/puppet/util/network_device/pure/device.rb new file mode 100644 index 0000000..109b7db --- /dev/null +++ b/lib/puppet/util/network_device/pure/device.rb @@ -0,0 +1,23 @@ +require 'puppet/util/network_device' +require 'puppet/util/network_device/pure' +require 'puppet/util/network_device/pure/facts' +require 'puppet/purestorage_api' + +class Puppet::Util::NetworkDevice::Pure::Device + + attr_accessor :transport + def initialize(url, option = {}) + @transport = PureStorageApi.new(url) + @url = url + Puppet.debug("Inside Device Initialize URL :" + url) + end + + def facts + Puppet.debug("Inside Device FACTS Initialize URL :" + @url) + @facts ||= Puppet::Util::NetworkDevice::Pure::Facts.new(@transport, @url) + Puppet.debug("After creating FACTS Object !!!") + thefacts = @facts.retrieve + thefacts + end + +end diff --git a/lib/puppet/util/network_device/pure/facts.rb b/lib/puppet/util/network_device/pure/facts.rb new file mode 100644 index 0000000..c9083d6 --- /dev/null +++ b/lib/puppet/util/network_device/pure/facts.rb @@ -0,0 +1,20 @@ +require 'puppet/util/network_device/pure' +require 'puppet/purestorage_api' + +class Puppet::Util::NetworkDevice::Pure::Facts + + attr_reader :transport, :url + def initialize(trasport, url) + @transport = transport + @url = url + Puppet.debug("Inside Initialize of Facts!") + end + + def retrieve + Puppet.debug("Retrieving Facts from fact.rb!") + @facts = {} + @facts["url"] = @url + @facts["vendor_id"] = 'pure' + @facts + end +end diff --git a/manifests/device.pp b/manifests/device.pp new file mode 100644 index 0000000..c796ebd --- /dev/null +++ b/manifests/device.pp @@ -0,0 +1,24 @@ +define pure::device ( + $hostname, + $username, + $password, + $target = undef, +) { + # validate_string($hostname) + # validate_string($username) + # validate_string($password) + + # $device_config = pick($target, $::settings::deviceconfig) + + #validate_absolute_path($device_config) + + augeas { "device.conf/${name}": + lens => 'Puppet_Device', + incl => $device_config, + context => $device_config, + changes => [ + 'set ${name}/type pure', + "set ${name}/url https://${username}:${password}@${hostname}", + ] + } +} diff --git a/manifests/init.pp b/manifests/init.pp new file mode 100644 index 0000000..deb8981 --- /dev/null +++ b/manifests/init.pp @@ -0,0 +1,48 @@ +# Class: pure +# =========================== +# +# Full description of class pure here. +# +# Parameters +# ---------- +# +# Document parameters here. +# +# * `sample parameter` +# Explanation of what this parameter affects and what it defaults to. +# e.g. "Specify one or more upstream ntp servers as an array." +# +# Variables +# ---------- +# +# Here you should define a list of variables that this module would require. +# +# * `sample variable` +# Explanation of how this variable affects the function of this class and if +# it has a default. e.g. "The parameter enc_ntp_servers must be set by the +# External Node Classifier as a comma separated list of hostnames." (Note, +# global variables should be avoided in favor of class parameters as +# of Puppet 2.6.) +# +# Examples +# -------- +# +# @example +# class { 'pure': +# servers => [ 'pool.ntp.org', 'ntp.local.company.com' ], +# } +# +# Authors +# ------- +# +# Author Name +# +# Copyright +# --------- +# +# Copyright 2017 Your name here, unless otherwise noted. +# +class pure { + + +} diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..9805d88 --- /dev/null +++ b/metadata.json @@ -0,0 +1,14 @@ +{ + "name": "pure-puppet", + "version": "0.1.0", + "author": "Pure Storage, Inc.", + "summary": "This is storage provisioning module and uses puppet device concept.", + "license": "Apache-2.0", + "source": "https://github.com/PureStorage-OpenConnect/pure-puppet", + "project_page": "on Pure Storage - OpenConnect", + "issues_url": null, + "dependencies": [ + {"name":"puppetlabs-stdlib","version_requirement":">= 1.0.0"} + ], + "data_provider": null +} diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb new file mode 100644 index 0000000..9bc077f --- /dev/null +++ b/spec/classes/init_spec.rb @@ -0,0 +1,6 @@ +require 'spec_helper' +describe 'pure' do + context 'with default values for all parameters' do + it { should contain_class('pure') } + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..2c6f566 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper'