Implementing a VMware Client with Extensible Records
by Matt Russell
What Makes up a Virtual Machine?
- name
- description
- number of cpus
- memory in megabytes
- status (running, off, suspended, etc)
- ....
Interacting with VMware
<retrievePropertiesRequest>
<ref>vm-444</ref>
<properties>
<property>name</property>
<property>hardware</property>
</properties>
</retrievePropertiesRequest>
Request
Response
<retrievePropertiesResponse>
<ref>vm-444</ref>
<properties>
<name>My-VM</name>
<hardware>
<numCpus>4</numCpus>
<memoryMb>4096</memoryMb>
</hardware>
</properties>
</retrievePropertiesResponse>
What should our Virtual Machine (VM) API look like?
-
Express all of the possible VM fields
-
Query only for the fields we care about
- Without Maybe for every field
Building Blocks of the VMware Client
-
Vinyl: Extensible record library using type level lists
-
Composite: Group of libraries focusing on making extensible records easy to work with
- HaXml: Utilities for parsing, filtering, transforming and generating XML documents
Our Virtual Machine Type
-- From Vinyl
data Rec :: (u -> *) -> [u] -> * where
RNil :: Rec f '[]
(:&) :: !(f r) -> !(Rec f rs) -> Rec f (r ': rs)
-- From Composite
newtype (:->) (s :: Symbol) a = Val { getVal :: a }
pattern (:*:) :: a -> Rec Identity rs -> Rec Identity ((s :-> a) ': rs)
let vm1 :: Rec Identity '[Name, Description, Hardware]
vm1 = "my-vm" :*: "Matt's awesome VM" :*: (Hardware 6 4096) :*: RNil
>>> view name vm1
>>> "my-vm" :: Text
type Name = "name" :-> Text
type Description = "description" :-> Text
type Hardware = "hardware" :-> Hardware
HaXml Schema Generation
class SchemaType a where
parseSchemaType :: XMLParser a
schemaTypeToXML :: a -> XMLContent
data Hardware = {
numCpus :: Int,
memoryMb :: Int
}
instance SchemaType Hardware where
parseSchemaType = ...
schemaTypeToXML = ...
Use HaXml to auto generate Haskell types from XSD files
Our Ideal API
let vm1 :: Rec Identity '[Name, Description, Hardware]
vm1 = retrieveProperties "vm-444" (Proxy @'[Name, Description, Hardware])
vm2 :: Rec Identity '[VMName, VMHardware]
vm2 = retrieveProperties "vm-444" (Proxy @'[VMName, VMHardware]
-- description is a lens from the extensible record to the description field
>>> view description vm1
>>> "Matt's awesome VM"
>>> view description vm2 --- type error!
Retrieve properties takes a proxy for the fields we want to request
Implementing Retrieve Properties
-- From Vinyl
class RecApplicative rs where
rpure :: (forall x. f x) -> Rec f rs
recordToList :: Rec (Const a) rs -> [a]
-- From Composite (slight oversimplification)
class ReifyNames (rs :: [*]) where
reifyNames :: Rec f rs -> Rec (Const Text) rs
recFromProxy :: forall rs proxy . RecApplicative rs
=> proxy rs -> Rec (Const ()) rs
recFromProxy _ = rpure $ Const ()
fieldNames :: forall rs proxy . (ReifyNames rs, RecApplicative rs, Functor f)
=> proxy rs -> [Text]
fieldNames = recordToList . reifyNames . recFromProxy
>>>> fieldNames (Proxy @'[Name, Description, Hardware])
>>>> ["name", "description", "hardware"]
Determining the "names" of the fields
Implementing Retrieve Properties
class RecordParserFromSchema rs where
recordParserFromSchema :: Rec XmlParser rs
instance forall s a (rs :: [*])
. (KnownSymbol s, SchemaType a, RecordParserFromSchema rs)
=> RecordParserFromSchema (s :-> a ': rs) where
recordParserFromSchema = (Val <$> parseSchemaType) :& recordFromSchema
instance RecordParserFromSchema '[] where
recordParserFromSchema = RNil
-- From Vinyl
rtraverse :: Applicative h
=> (forall x. f x -> h (g x)) -> Rec f rs -> h (Rec g rs)
propertiesParser :: RecordParserFromSchema rs
=> proxy rs -> XmlParser (Rec Identity rs)
propertiesParser _ = rtraverse (Identity <$>) recordParserFromSchema
Determining the parsers of the fields
Implementing Retrieve Properties
propertiesParser :: RecordParserFromSchema rs
=> XmlParser (Rec Identity rs)
fieldNames :: forall rs proxy
. (ReifyNames rs, RecApplicative rs, Functor f)
=> proxy rs -> [Text]
retrieveProperties :: (RecordParserFromSchema rs, ReifyNames rs, RecApplicative rs)
=> proxy rs -> Text -> IO (Either ParseError (Rec Identity rs))
retrieveProperties p ref = do
let requestFields = fieldNames p
responseParser = propertiesParser p
resp <- sendRequest ref requestFields
return (responseParser fieldNames)
Putting it all together
Other cool things in the VMware Client
- References to VMs (and other objects) are newtypes with safe constructors
- Type safety to ensure we can only request properties for fields on the objects
- Using HaXml to autogenerate types and endpoints
- Using Classy Lenses to work with VMware's OO API
Thank You!
Let's chat at ICFP!
or connect after:
matt@simspace.com
@mrussell247
Implementing a VMware Client with Extensible Records
By Matt Russell
Implementing a VMware Client with Extensible Records
- 2,521